@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.mjs CHANGED
@@ -15,7 +15,7 @@ var SagepilotChatError = class extends Error {
15
15
 
16
16
  // src/core/config/constants.ts
17
17
  var SDK_NAME = "@sagepilot-ai/react-native-sdk";
18
- var SDK_VERSION = "0.2.4";
18
+ var SDK_VERSION = "0.2.5";
19
19
  var DEFAULT_HOST = "https://app.sagepilot.ai";
20
20
  var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
21
21
  var CUSTOMER_API_PREFIX = "/customer-api/v1";
@@ -532,6 +532,9 @@ var SagepilotReactNativeChat = class {
532
532
  const { workspaceId, channelId } = parseKey(config.key);
533
533
  this.runtimeOptions = {
534
534
  behavior: config.behavior,
535
+ cacheStorage: config.cacheStorage,
536
+ filePicker: config.filePicker,
537
+ fileStore: config.fileStore,
535
538
  presentation: config.presentation,
536
539
  theme: config.theme,
537
540
  hostedClaimsStorageKeyPrefix: config.hostedClaimsStorageKeyPrefix || DEFAULT_HOSTED_CLAIMS_STORAGE_KEY_PREFIX
@@ -1146,14 +1149,295 @@ function createAsyncStorageCacheStorage(asyncStorage) {
1146
1149
  removeItem: (key) => asyncStorage.removeItem(key)
1147
1150
  };
1148
1151
  }
1152
+ function createJsonCache(storage, namespace) {
1153
+ return {
1154
+ async get(key) {
1155
+ const value = await storage.getItem(`${namespace}:${key}`);
1156
+ if (!value) return null;
1157
+ try {
1158
+ return JSON.parse(value);
1159
+ } catch {
1160
+ return null;
1161
+ }
1162
+ },
1163
+ async set(key, value) {
1164
+ await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
1165
+ },
1166
+ async remove(key) {
1167
+ await storage.removeItem(`${namespace}:${key}`);
1168
+ }
1169
+ };
1170
+ }
1171
+
1172
+ // src/core/storage/fileStore.ts
1173
+ var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
1174
+ function sanitizeKey(key) {
1175
+ return key.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/\.{2,}/g, "_");
1176
+ }
1177
+ function createSagepilotFileStore(blobUtil, options = {}) {
1178
+ const directory = `${blobUtil.fs.dirs.DocumentDir}/${options.directoryName ?? DEFAULT_DIRECTORY_NAME}`;
1179
+ const pathFor = (key) => `${directory}/${sanitizeKey(key)}`;
1180
+ let dirReady = null;
1181
+ function ensureDirectory() {
1182
+ if (!dirReady) {
1183
+ dirReady = (async () => {
1184
+ if (!await blobUtil.fs.exists(directory)) {
1185
+ await blobUtil.fs.mkdir(directory);
1186
+ }
1187
+ })().catch((error) => {
1188
+ dirReady = null;
1189
+ throw error;
1190
+ });
1191
+ }
1192
+ return dirReady;
1193
+ }
1194
+ return {
1195
+ async write(key, base64) {
1196
+ await ensureDirectory();
1197
+ await blobUtil.fs.writeFile(pathFor(key), base64, "base64");
1198
+ },
1199
+ async read(key) {
1200
+ const content = await blobUtil.fs.readFile(pathFor(key), "base64");
1201
+ if (typeof content !== "string" || content.length === 0) {
1202
+ throw new Error("Persisted attachment is empty or unreadable.");
1203
+ }
1204
+ return content;
1205
+ },
1206
+ async remove(key) {
1207
+ try {
1208
+ if (await blobUtil.fs.exists(pathFor(key))) {
1209
+ await blobUtil.fs.unlink(pathFor(key));
1210
+ }
1211
+ } catch {
1212
+ }
1213
+ },
1214
+ async prune(keepKeys) {
1215
+ try {
1216
+ if (!await blobUtil.fs.exists(directory)) return;
1217
+ const keep = new Set(keepKeys.map(sanitizeKey));
1218
+ const entries = await blobUtil.fs.ls(directory);
1219
+ await Promise.all(
1220
+ entries.filter((entry) => !keep.has(entry)).map((entry) => blobUtil.fs.unlink(`${directory}/${entry}`).catch(() => void 0))
1221
+ );
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ };
1226
+ }
1227
+
1228
+ // src/core/native/filePicker.ts
1229
+ import { PermissionsAndroid, Platform } from "react-native";
1230
+ var DEFAULT_IMAGE_MAX_DIMENSION = 1920;
1231
+ var DEFAULT_IMAGE_QUALITY = 0.8;
1232
+ var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
1233
+ var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
1234
+ var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
1235
+ var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1236
+ var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
1237
+ var SagepilotFilePickerError = class extends Error {
1238
+ constructor(code, message) {
1239
+ super(message);
1240
+ this.name = "SagepilotFilePickerError";
1241
+ this.code = code;
1242
+ }
1243
+ };
1244
+ function bytesToMb(bytes) {
1245
+ return Math.round(bytes / (1024 * 1024) * 10) / 10;
1246
+ }
1247
+ function estimateBase64ByteSize(dataBase64) {
1248
+ return Math.floor(dataBase64.length * 3 / 4);
1249
+ }
1250
+ function stripFileScheme(uri) {
1251
+ return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
1252
+ }
1253
+ async function ensureAndroidCameraPermission() {
1254
+ if (Platform.OS !== "android") return;
1255
+ let result;
1256
+ try {
1257
+ result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
1258
+ } catch {
1259
+ return;
1260
+ }
1261
+ if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
1262
+ throw new SagepilotFilePickerError(
1263
+ "permission_denied",
1264
+ "Camera access is turned off. Enable it for this app in Settings to take a photo."
1265
+ );
1266
+ }
1267
+ if (result === PermissionsAndroid.RESULTS.DENIED) {
1268
+ throw new SagepilotFilePickerError(
1269
+ "permission_denied",
1270
+ "Camera permission is required to take a photo."
1271
+ );
1272
+ }
1273
+ }
1274
+ function mapImagePickerErrorCode(errorCode) {
1275
+ switch (errorCode) {
1276
+ case "permission":
1277
+ return "permission_denied";
1278
+ case "camera_unavailable":
1279
+ return "camera_unavailable";
1280
+ default:
1281
+ return "unknown";
1282
+ }
1283
+ }
1284
+ function imageAssetsToPickedFiles(result, maxFileSizeBytes) {
1285
+ if (result.didCancel) return [];
1286
+ if (result.errorCode) {
1287
+ throw new SagepilotFilePickerError(
1288
+ mapImagePickerErrorCode(result.errorCode),
1289
+ result.errorMessage || `Could not capture the photo (${result.errorCode}).`
1290
+ );
1291
+ }
1292
+ const assets = result.assets ?? [];
1293
+ const usable = assets.filter((asset) => typeof asset.base64 === "string" && asset.base64.length > 0);
1294
+ if (assets.length > usable.length) {
1295
+ throw new SagepilotFilePickerError(
1296
+ "encode_failed",
1297
+ assets.length === 1 ? "The photo could not be processed. Please try again." : "Some photos could not be processed. Please try selecting them again."
1298
+ );
1299
+ }
1300
+ return usable.map((asset, index) => {
1301
+ const dataBase64 = asset.base64;
1302
+ const size = asset.fileSize ?? estimateBase64ByteSize(dataBase64);
1303
+ if (maxFileSizeBytes > 0 && size > maxFileSizeBytes) {
1304
+ throw new SagepilotFilePickerError(
1305
+ "file_too_large",
1306
+ `Image is too large (max ${bytesToMb(maxFileSizeBytes)}MB).`
1307
+ );
1308
+ }
1309
+ return {
1310
+ file_name: asset.fileName || `photo-${Date.now()}-${index + 1}.jpg`,
1311
+ mime_type: asset.type || DEFAULT_IMAGE_MIME_TYPE,
1312
+ size,
1313
+ data_base64: dataBase64
1314
+ };
1315
+ });
1316
+ }
1317
+ async function readUriAsBase64(uri, fileReader) {
1318
+ if (fileReader) {
1319
+ const content = await fileReader.fs.readFile(stripFileScheme(uri), "base64");
1320
+ if (typeof content !== "string") {
1321
+ throw new Error("File reader returned unexpected content.");
1322
+ }
1323
+ return content;
1324
+ }
1325
+ const response = await fetch(uri);
1326
+ const blob = await response.blob();
1327
+ return new Promise((resolve, reject) => {
1328
+ const reader = new FileReader();
1329
+ reader.onload = () => {
1330
+ const dataUrl = typeof reader.result === "string" ? reader.result : "";
1331
+ const [, payload = ""] = dataUrl.split(",");
1332
+ if (!payload) {
1333
+ reject(new Error("Could not read the selected file."));
1334
+ return;
1335
+ }
1336
+ resolve(payload);
1337
+ };
1338
+ reader.onerror = () => reject(new Error("Could not read the selected file."));
1339
+ reader.readAsDataURL(blob);
1340
+ });
1341
+ }
1342
+ function createSagepilotFilePicker(options) {
1343
+ const { imagePicker, documentsPicker, fileReader } = options;
1344
+ const imageMaxDimension = options.imageMaxDimension ?? DEFAULT_IMAGE_MAX_DIMENSION;
1345
+ const imageQuality = options.imageQuality ?? DEFAULT_IMAGE_QUALITY;
1346
+ const imageSelectionLimit = options.imageSelectionLimit ?? DEFAULT_IMAGE_SELECTION_LIMIT;
1347
+ const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
1348
+ const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1349
+ const sources = [];
1350
+ if (imagePicker) sources.push("camera", "library");
1351
+ if (documentsPicker) sources.push("documents");
1352
+ if (sources.length === 0) return void 0;
1353
+ const imageOptions = {
1354
+ mediaType: "photo",
1355
+ includeBase64: true,
1356
+ maxWidth: imageMaxDimension,
1357
+ maxHeight: imageMaxDimension,
1358
+ quality: imageQuality,
1359
+ saveToPhotos: false
1360
+ };
1361
+ async function pickFromCamera() {
1362
+ if (!imagePicker) return [];
1363
+ await ensureAndroidCameraPermission();
1364
+ return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
1365
+ }
1366
+ async function pickFromLibrary(multiple) {
1367
+ if (!imagePicker) return [];
1368
+ return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
1369
+ ...imageOptions,
1370
+ // Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
1371
+ selectionLimit: multiple ? Math.max(0, imageSelectionLimit) : 1
1372
+ }), imageMaxFileSizeBytes);
1373
+ }
1374
+ async function pickDocuments(multiple) {
1375
+ if (!documentsPicker) return [];
1376
+ let picked;
1377
+ try {
1378
+ picked = await documentsPicker.pick({ allowMultiSelection: multiple });
1379
+ } catch (error) {
1380
+ if (error && typeof error === "object" && error.code === "OPERATION_CANCELED") {
1381
+ return [];
1382
+ }
1383
+ throw error;
1384
+ }
1385
+ const files = [];
1386
+ for (const file of picked) {
1387
+ if (documentMaxFileSizeBytes > 0 && typeof file.size === "number" && file.size > documentMaxFileSizeBytes) {
1388
+ throw new SagepilotFilePickerError(
1389
+ "file_too_large",
1390
+ `"${file.name || "File"}" is too large (max ${bytesToMb(documentMaxFileSizeBytes)}MB).`
1391
+ );
1392
+ }
1393
+ let readableUri = file.uri;
1394
+ if (documentsPicker.keepLocalCopy) {
1395
+ const fileName = file.name || `file-${Date.now()}`;
1396
+ const [copy] = await documentsPicker.keepLocalCopy({
1397
+ files: [{ uri: file.uri, fileName }],
1398
+ destination: "cachesDirectory"
1399
+ });
1400
+ if (copy?.status === "success" && copy.localUri) {
1401
+ readableUri = copy.localUri;
1402
+ }
1403
+ }
1404
+ let dataBase64;
1405
+ try {
1406
+ dataBase64 = await readUriAsBase64(readableUri, fileReader);
1407
+ } catch (error) {
1408
+ throw new SagepilotFilePickerError(
1409
+ "read_failed",
1410
+ error instanceof Error && error.message ? error.message : "Could not read the selected file."
1411
+ );
1412
+ }
1413
+ files.push({
1414
+ file_name: file.name || `file-${Date.now()}`,
1415
+ mime_type: file.type || DEFAULT_DOCUMENT_MIME_TYPE,
1416
+ size: file.size ?? estimateBase64ByteSize(dataBase64),
1417
+ data_base64: dataBase64
1418
+ });
1419
+ }
1420
+ return files;
1421
+ }
1422
+ return {
1423
+ sources,
1424
+ async pickFiles(request) {
1425
+ if (request.source === "camera") return pickFromCamera();
1426
+ if (request.source === "library") return pickFromLibrary(request.multiple);
1427
+ return pickDocuments(request.multiple);
1428
+ }
1429
+ };
1430
+ }
1149
1431
 
1150
1432
  // src/ui/SagepilotChatProvider.ts
1151
1433
  import { createElement, useCallback, useEffect, useRef, useState } from "react";
1152
1434
  import {
1153
1435
  ActivityIndicator,
1436
+ AppState,
1154
1437
  KeyboardAvoidingView,
1438
+ Linking,
1155
1439
  Modal,
1156
- Platform,
1440
+ Platform as Platform2,
1157
1441
  Pressable,
1158
1442
  SafeAreaView,
1159
1443
  StyleSheet,
@@ -1163,6 +1447,37 @@ import {
1163
1447
  import { WebView } from "react-native-webview";
1164
1448
 
1165
1449
  // src/core/webview/mobileBridge.ts
1450
+ var FILE_PICKER_PROTOCOL_VERSION = 2;
1451
+ function buildBridgeCapabilitiesScript(capabilities) {
1452
+ const payload = { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1453
+ return [
1454
+ "(function(){",
1455
+ "try {",
1456
+ "if (window.SagepilotMobileBridge) {",
1457
+ `window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, ${JSON.stringify(payload)});`,
1458
+ "}",
1459
+ "} catch (_) {}",
1460
+ "true;",
1461
+ "})();"
1462
+ ].join("\n");
1463
+ }
1464
+ function buildLivenessPingScript(nonce, staleMs) {
1465
+ return [
1466
+ "(function(){",
1467
+ "try {",
1468
+ "var hb = window.__sagepilotWidgetHeartbeat;",
1469
+ "var supports = typeof hb === 'number';",
1470
+ `var alive = !supports ? true : (Date.now() - hb < ${Math.max(0, Math.floor(staleMs))});`,
1471
+ "var send = function(){",
1472
+ " var msg = JSON.stringify({ type: 'sagepilot:pong', nonce: " + JSON.stringify(nonce) + ", alive: alive, supportsHeartbeat: supports });",
1473
+ " if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { window.ReactNativeWebView.postMessage(msg); }",
1474
+ "};",
1475
+ "send();",
1476
+ "} catch (_) {}",
1477
+ "true;",
1478
+ "})();"
1479
+ ].join("\n");
1480
+ }
1166
1481
  function isHostedBridgeMessage(value) {
1167
1482
  if (!value || typeof value !== "object") return false;
1168
1483
  const message = value;
@@ -1288,9 +1603,70 @@ function readPresentationState() {
1288
1603
  function getInjectedWebViewScript() {
1289
1604
  return [
1290
1605
  internalSagepilotChat.getHostedAuthScript(),
1291
- mobileWebViewBridgeScript
1606
+ mobileWebViewBridgeScript,
1607
+ buildBridgeCapabilitiesScript({
1608
+ nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1609
+ })
1292
1610
  ].filter(Boolean).join("\n");
1293
1611
  }
1612
+ var FILE_PICKER_SOURCE_LABELS = {
1613
+ camera: "Take photo",
1614
+ library: "Choose from gallery",
1615
+ documents: "Browse files"
1616
+ };
1617
+ var DELIVERY_RETRY_MS = 1500;
1618
+ var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
1619
+ var DELIVERY_MAX_ATTEMPTS = 8;
1620
+ var PERSIST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
1621
+ var PENDING_FILES_CACHE_NAMESPACE = "sagepilot_rn_picker";
1622
+ var PENDING_FILES_CACHE_KEY = "pending_batch";
1623
+ var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
1624
+ var LIVENESS_PONG_TIMEOUT_MS = 1500;
1625
+ var LIVENESS_MAX_PING_ATTEMPTS = 2;
1626
+ var LIVENESS_MAX_REMOUNTS = 2;
1627
+ var batchSequence = 0;
1628
+ function nextBatchId() {
1629
+ batchSequence += 1;
1630
+ const entropy = Math.random().toString(36).slice(2, 8);
1631
+ return `b_${Date.now().toString(36)}_${batchSequence}_${entropy}`;
1632
+ }
1633
+ function totalBase64Bytes(files) {
1634
+ return files.reduce((sum, file) => sum + (file.data_base64?.length ?? 0), 0);
1635
+ }
1636
+ function storageKeyFor(batchId, index) {
1637
+ return `${batchId}_${index}.bin`;
1638
+ }
1639
+ function buildDispatchScript(messageLiteral) {
1640
+ return [
1641
+ "(function(){",
1642
+ "try {",
1643
+ `var message = ${messageLiteral};`,
1644
+ "window.dispatchEvent(new MessageEvent('message', { data: message, origin: window.location.origin, source: window.parent || window }));",
1645
+ "} catch (_) {}",
1646
+ "true;",
1647
+ "})();"
1648
+ ].join("\n");
1649
+ }
1650
+ function buildFilesPickedScript(files, batchId) {
1651
+ return buildDispatchScript(`{ type: "sagepilot:files_picked", batch_id: ${JSON.stringify(batchId)}, files: ${JSON.stringify(files)} }`);
1652
+ }
1653
+ function buildFilePickerErrorScript(message, code) {
1654
+ return buildDispatchScript(
1655
+ `{ type: "sagepilot:file_picker_error", message: ${JSON.stringify(message)}${code ? `, code: ${JSON.stringify(code)}` : ""} }`
1656
+ );
1657
+ }
1658
+ function buildFilePickerCancelledScript() {
1659
+ return buildDispatchScript(`{ type: "sagepilot:file_picker_cancelled" }`);
1660
+ }
1661
+ function readFilePickerError(error) {
1662
+ if (error && typeof error === "object") {
1663
+ const candidate = error;
1664
+ const message = typeof candidate.message === "string" && candidate.message ? candidate.message : "Could not attach the selected file.";
1665
+ const code = typeof candidate.code === "string" ? candidate.code : void 0;
1666
+ return { message, code };
1667
+ }
1668
+ return { message: "Could not attach the selected file." };
1669
+ }
1294
1670
  function getHostedIdentityDispatchScript() {
1295
1671
  const message = internalSagepilotChat.getHostedIdentityMessage();
1296
1672
  if (!message) return "";
@@ -1313,10 +1689,13 @@ function readUrlOrigin(url) {
1313
1689
  return null;
1314
1690
  }
1315
1691
  }
1692
+ function isInternalWebViewScheme(url) {
1693
+ return url.startsWith("about:") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:") || url.startsWith("file:");
1694
+ }
1316
1695
  var hostedChatWebViewProps = {
1317
1696
  allowFileAccess: true,
1318
1697
  allowFileAccessFromFileURLs: true,
1319
- ...Platform.OS === "ios" ? {
1698
+ ...Platform2.OS === "ios" ? {
1320
1699
  automaticallyAdjustContentInsets: false,
1321
1700
  contentInsetAdjustmentBehavior: "never",
1322
1701
  hideKeyboardAccessoryView: true
@@ -1331,7 +1710,7 @@ var hostedChatWebViewProps = {
1331
1710
  sharedCookiesEnabled: true,
1332
1711
  thirdPartyCookiesEnabled: true
1333
1712
  };
1334
- var AndroidInsetsView = Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
1713
+ var AndroidInsetsView = Platform2.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
1335
1714
  var emptyAndroidInsets = { top: 0, bottom: 0 };
1336
1715
  function SagepilotChatProvider({
1337
1716
  children,
@@ -1340,13 +1719,292 @@ function SagepilotChatProvider({
1340
1719
  }) {
1341
1720
  const [state, setState] = useState(readPresentationState);
1342
1721
  const [androidModalInsets, setAndroidModalInsets] = useState(emptyAndroidInsets);
1722
+ const [nativeWebViewKey, setNativeWebViewKey] = useState(0);
1723
+ const [preloadWebViewKey, setPreloadWebViewKey] = useState(0);
1343
1724
  const webFrameRef = useRef(null);
1344
1725
  const nativeWebViewRef = useRef(null);
1726
+ const pendingBatchesRef = useRef([]);
1727
+ const widgetReadyRef = useRef(false);
1728
+ const deliveryTimerRef = useRef(null);
1729
+ const deliveryAttemptsRef = useRef(0);
1730
+ const pendingPingRef = useRef(null);
1731
+ const pingTimeoutRef = useRef(null);
1732
+ const livenessRemountCountRef = useRef(0);
1733
+ const appStateRef = useRef(AppState.currentState);
1734
+ const didReconcileRef = useRef(false);
1735
+ const [androidRepaintTick, setAndroidRepaintTick] = useState(0);
1736
+ const [sourceChooser, setSourceChooser] = useState(null);
1737
+ const getPendingCache = useCallback(() => {
1738
+ const cacheStorage = internalSagepilotChat.getConfig()?.cacheStorage;
1739
+ if (!cacheStorage) return null;
1740
+ return createJsonCache(cacheStorage, PENDING_FILES_CACHE_NAMESPACE);
1741
+ }, []);
1742
+ const writeManifest = useCallback(() => {
1743
+ const cache = getPendingCache();
1744
+ if (!cache) return;
1745
+ const queue = pendingBatchesRef.current;
1746
+ if (queue.length === 0) {
1747
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
1748
+ return;
1749
+ }
1750
+ const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
1751
+ if (hasFileStore) {
1752
+ const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
1753
+ batchId: batch.batchId,
1754
+ files: batch.files.map((file, index) => ({
1755
+ file_name: file.file_name,
1756
+ mime_type: file.mime_type,
1757
+ size: file.size,
1758
+ storageKey: batch.storageKeys[index]
1759
+ }))
1760
+ }));
1761
+ if (manifest.length === 0) {
1762
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
1763
+ } else {
1764
+ void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
1765
+ }
1766
+ return;
1767
+ }
1768
+ const totalBytes = queue.reduce((sum, batch) => sum + totalBase64Bytes(batch.files), 0);
1769
+ if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
1770
+ const manifest = queue.map((batch) => ({
1771
+ batchId: batch.batchId,
1772
+ files: batch.files.map((file) => ({
1773
+ file_name: file.file_name,
1774
+ mime_type: file.mime_type,
1775
+ size: file.size,
1776
+ data_base64: file.data_base64
1777
+ }))
1778
+ }));
1779
+ void cache.set(PENDING_FILES_CACHE_KEY, manifest).catch(() => void 0);
1780
+ } else {
1781
+ void cache.remove(PENDING_FILES_CACHE_KEY).catch(() => void 0);
1782
+ }
1783
+ }, [getPendingCache]);
1784
+ const writeBatchBytes = useCallback(async (batch) => {
1785
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
1786
+ if (!fileStore) return;
1787
+ const entries = batch.files.map((file, index) => ({
1788
+ key: storageKeyFor(batch.batchId, index),
1789
+ base64: file.data_base64
1790
+ }));
1791
+ try {
1792
+ await Promise.all(entries.map((entry) => fileStore.write(entry.key, entry.base64)));
1793
+ } catch {
1794
+ return;
1795
+ }
1796
+ if (!pendingBatchesRef.current.some((queued) => queued.batchId === batch.batchId)) {
1797
+ void Promise.all(entries.map((entry) => fileStore.remove(entry.key).catch(() => void 0)));
1798
+ }
1799
+ }, []);
1800
+ const discardBatchFiles = useCallback((batch) => {
1801
+ if (!batch?.storageKeys) return;
1802
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
1803
+ if (!fileStore) return;
1804
+ void Promise.all(batch.storageKeys.map((key) => fileStore.remove(key).catch(() => void 0)));
1805
+ }, []);
1806
+ const pumpDelivery = useCallback(() => {
1807
+ if (deliveryTimerRef.current) {
1808
+ clearTimeout(deliveryTimerRef.current);
1809
+ deliveryTimerRef.current = null;
1810
+ }
1811
+ const batch = pendingBatchesRef.current[0];
1812
+ if (!batch) return;
1813
+ deliveryAttemptsRef.current += 1;
1814
+ const attempts = deliveryAttemptsRef.current;
1815
+ const ref = nativeWebViewRef.current;
1816
+ const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
1817
+ if (shouldDeliver && ref) {
1818
+ ref.injectJavaScript(buildFilesPickedScript(batch.files, batch.batchId));
1819
+ }
1820
+ if (attempts < DELIVERY_MAX_ATTEMPTS) {
1821
+ deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
1822
+ }
1823
+ }, []);
1824
+ const startDelivery = useCallback(() => {
1825
+ deliveryAttemptsRef.current = 0;
1826
+ pumpDelivery();
1827
+ }, [pumpDelivery]);
1828
+ const ensureDelivery = useCallback(() => {
1829
+ if (pendingBatchesRef.current.length === 0) return;
1830
+ if (deliveryTimerRef.current) return;
1831
+ startDelivery();
1832
+ }, [startDelivery]);
1833
+ const acknowledgeHeadBatch = useCallback(() => {
1834
+ const head = pendingBatchesRef.current[0];
1835
+ pendingBatchesRef.current = pendingBatchesRef.current.slice(1);
1836
+ deliveryAttemptsRef.current = 0;
1837
+ if (deliveryTimerRef.current) {
1838
+ clearTimeout(deliveryTimerRef.current);
1839
+ deliveryTimerRef.current = null;
1840
+ }
1841
+ discardBatchFiles(head);
1842
+ writeManifest();
1843
+ if (pendingBatchesRef.current.length > 0) startDelivery();
1844
+ }, [discardBatchFiles, writeManifest, startDelivery]);
1845
+ const queuePickedFiles = useCallback((files) => {
1846
+ const batchId = nextBatchId();
1847
+ const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
1848
+ const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
1849
+ const batch = { batchId, files, storageKeys };
1850
+ pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
1851
+ ensureDelivery();
1852
+ writeManifest();
1853
+ void writeBatchBytes(batch);
1854
+ }, [ensureDelivery, writeManifest, writeBatchBytes]);
1855
+ const recoverNativeWebView = useCallback(() => {
1856
+ widgetReadyRef.current = false;
1857
+ setNativeWebViewKey((key) => key + 1);
1858
+ }, []);
1859
+ const recoverPreloadWebView = useCallback(() => {
1860
+ setPreloadWebViewKey((key) => key + 1);
1861
+ }, []);
1862
+ const runNativeFilePicker = useCallback((source, multiple) => {
1863
+ const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1864
+ if (!filePicker) return;
1865
+ filePicker.pickFiles({ source, multiple }).then((files) => {
1866
+ if (files.length === 0) {
1867
+ nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
1868
+ return;
1869
+ }
1870
+ queuePickedFiles(files);
1871
+ }).catch((error) => {
1872
+ const { message, code } = readFilePickerError(error);
1873
+ nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
1874
+ });
1875
+ }, [queuePickedFiles]);
1876
+ const openNativeFilePicker = useCallback((multiple) => {
1877
+ const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1878
+ if (!filePicker || filePicker.sources.length === 0) return;
1879
+ const [onlySource] = filePicker.sources;
1880
+ if (filePicker.sources.length === 1 && onlySource) {
1881
+ runNativeFilePicker(onlySource, multiple);
1882
+ return;
1883
+ }
1884
+ setSourceChooser({ multiple });
1885
+ }, [runNativeFilePicker]);
1886
+ const handleSourceChoice = useCallback((source) => {
1887
+ const chooser = sourceChooser;
1888
+ setSourceChooser(null);
1889
+ if (chooser) runNativeFilePicker(source, chooser.multiple);
1890
+ }, [sourceChooser, runNativeFilePicker]);
1345
1891
  useEffect(() => {
1346
1892
  return internalSagepilotChat.onStateChange(() => {
1347
1893
  setState(readPresentationState());
1348
1894
  });
1349
1895
  }, []);
1896
+ useEffect(() => {
1897
+ let cancelled = false;
1898
+ const cache = getPendingCache();
1899
+ if (!cache) return;
1900
+ const reconcile = async () => {
1901
+ if (didReconcileRef.current) return;
1902
+ const manifest = await cache.get(PENDING_FILES_CACHE_KEY).catch(() => null);
1903
+ if (cancelled || didReconcileRef.current || !Array.isArray(manifest) || manifest.length === 0) return;
1904
+ const fileStore = internalSagepilotChat.getConfig()?.fileStore;
1905
+ const restored = [];
1906
+ for (const batch of manifest) {
1907
+ if (!batch || typeof batch.batchId !== "string" || !Array.isArray(batch.files) || batch.files.length === 0) continue;
1908
+ const files = [];
1909
+ const storageKeys = [];
1910
+ let intact = true;
1911
+ for (const file of batch.files) {
1912
+ let dataBase64 = null;
1913
+ if (typeof file.storageKey === "string" && fileStore) {
1914
+ try {
1915
+ dataBase64 = await fileStore.read(file.storageKey);
1916
+ storageKeys.push(file.storageKey);
1917
+ } catch {
1918
+ intact = false;
1919
+ }
1920
+ } else if (typeof file.data_base64 === "string" && file.data_base64) {
1921
+ dataBase64 = file.data_base64;
1922
+ }
1923
+ if (!dataBase64) {
1924
+ intact = false;
1925
+ break;
1926
+ }
1927
+ files.push({ file_name: file.file_name, mime_type: file.mime_type, size: file.size, data_base64: dataBase64 });
1928
+ }
1929
+ if (intact && files.length === batch.files.length) {
1930
+ restored.push({
1931
+ batchId: batch.batchId,
1932
+ files,
1933
+ storageKeys: storageKeys.length === files.length ? storageKeys : void 0
1934
+ });
1935
+ }
1936
+ }
1937
+ if (cancelled || didReconcileRef.current) return;
1938
+ didReconcileRef.current = true;
1939
+ pendingBatchesRef.current = [...restored, ...pendingBatchesRef.current];
1940
+ writeManifest();
1941
+ if (fileStore) {
1942
+ const keep = pendingBatchesRef.current.flatMap(
1943
+ (batch) => batch.files.map((_, index) => storageKeyFor(batch.batchId, index))
1944
+ );
1945
+ void fileStore.prune(keep).catch(() => void 0);
1946
+ }
1947
+ startDelivery();
1948
+ };
1949
+ void reconcile();
1950
+ return () => {
1951
+ cancelled = true;
1952
+ };
1953
+ }, [getPendingCache, startDelivery, writeManifest]);
1954
+ useEffect(() => {
1955
+ return () => {
1956
+ if (deliveryTimerRef.current) {
1957
+ clearTimeout(deliveryTimerRef.current);
1958
+ deliveryTimerRef.current = null;
1959
+ }
1960
+ if (pingTimeoutRef.current) {
1961
+ clearTimeout(pingTimeoutRef.current);
1962
+ pingTimeoutRef.current = null;
1963
+ }
1964
+ };
1965
+ }, []);
1966
+ const clearPing = useCallback(() => {
1967
+ pendingPingRef.current = null;
1968
+ if (pingTimeoutRef.current) {
1969
+ clearTimeout(pingTimeoutRef.current);
1970
+ pingTimeoutRef.current = null;
1971
+ }
1972
+ }, []);
1973
+ const runLivenessProbe = useCallback(() => {
1974
+ if (Platform2.OS !== "android") return;
1975
+ const ref = nativeWebViewRef.current;
1976
+ if (!ref || !internalSagepilotChat.isPresented()) return;
1977
+ setAndroidRepaintTick((tick) => tick + 1);
1978
+ const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
1979
+ const nonce = `${Date.now().toString(36)}_${attempts}`;
1980
+ pendingPingRef.current = { nonce, attempts };
1981
+ ref.injectJavaScript(buildLivenessPingScript(nonce, LIVENESS_HEARTBEAT_STALE_MS));
1982
+ if (pingTimeoutRef.current) clearTimeout(pingTimeoutRef.current);
1983
+ pingTimeoutRef.current = setTimeout(() => {
1984
+ if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
1985
+ clearPing();
1986
+ recoverNativeWebView();
1987
+ return;
1988
+ }
1989
+ runLivenessProbe();
1990
+ }, LIVENESS_PONG_TIMEOUT_MS);
1991
+ }, [clearPing, recoverNativeWebView]);
1992
+ useEffect(() => {
1993
+ const subscription = AppState.addEventListener("change", (nextState) => {
1994
+ const prev = appStateRef.current;
1995
+ appStateRef.current = nextState;
1996
+ if (nextState === "active" && (prev === "background" || prev === "inactive")) {
1997
+ clearPing();
1998
+ livenessRemountCountRef.current = 0;
1999
+ runLivenessProbe();
2000
+ ensureDelivery();
2001
+ }
2002
+ });
2003
+ return () => {
2004
+ subscription.remove();
2005
+ clearPing();
2006
+ };
2007
+ }, [clearPing, runLivenessProbe, ensureDelivery]);
1350
2008
  const handleAndroidInsetsChange = useCallback((event) => {
1351
2009
  const nextBottomInset = event.nativeEvent?.bottom;
1352
2010
  const nextTopInset = event.nativeEvent?.top;
@@ -1360,11 +2018,11 @@ function SagepilotChatProvider({
1360
2018
  const presentationStyle = state.presentation?.style ?? "sheet";
1361
2019
  const isFullScreenModal = presentationStyle === "fullScreen";
1362
2020
  const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
1363
- const ModalContainer = Platform.OS === "ios" && isFullScreenModal ? SafeAreaView : View;
1364
- const NativeModalContainer = Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
1365
- const ChatContentContainer = Platform.OS === "ios" ? KeyboardAvoidingView : View;
1366
- const nativeModalContainerProps = Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
1367
- const chatContentContainerProps = Platform.OS === "ios" ? {
2021
+ const ModalContainer = Platform2.OS === "ios" && isFullScreenModal ? SafeAreaView : View;
2022
+ const NativeModalContainer = Platform2.OS === "android" ? AndroidInsetsView : ModalContainer;
2023
+ const ChatContentContainer = Platform2.OS === "ios" ? KeyboardAvoidingView : View;
2024
+ const nativeModalContainerProps = Platform2.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
2025
+ const chatContentContainerProps = Platform2.OS === "ios" ? {
1368
2026
  behavior: "padding",
1369
2027
  enabled: true,
1370
2028
  keyboardVerticalOffset: 0,
@@ -1372,14 +2030,48 @@ function SagepilotChatProvider({
1372
2030
  } : {
1373
2031
  style: [
1374
2032
  styles.modalContent,
1375
- Platform.OS === "android" ? {
2033
+ Platform2.OS === "android" ? {
1376
2034
  paddingTop: androidModalInsets.top,
1377
2035
  paddingBottom: androidModalInsets.bottom
1378
2036
  } : null
1379
2037
  ].filter(Boolean)
1380
2038
  };
1381
2039
  const handleWebViewMessage = (event) => {
1382
- internalSagepilotChat.handleHostedBridgeMessage(parseHostedBridgeMessage(event.nativeEvent?.data));
2040
+ const message = parseHostedBridgeMessage(event.nativeEvent?.data);
2041
+ if (message?.type === "sagepilot:open_file_picker") {
2042
+ openNativeFilePicker(message.multiple ?? true);
2043
+ return;
2044
+ }
2045
+ if (message?.type === "sagepilot:widget_listener_ready") {
2046
+ widgetReadyRef.current = true;
2047
+ startDelivery();
2048
+ return;
2049
+ }
2050
+ if (message?.type === "sagepilot:files_received") {
2051
+ const ackBatchId = message.batch_id;
2052
+ const head = pendingBatchesRef.current[0];
2053
+ if (head && (!ackBatchId || ackBatchId === head.batchId)) {
2054
+ acknowledgeHeadBatch();
2055
+ }
2056
+ return;
2057
+ }
2058
+ if (message?.type === "sagepilot:pong") {
2059
+ const pending = pendingPingRef.current;
2060
+ if (pending && (!message.nonce || message.nonce === pending.nonce)) {
2061
+ if (message.alive === false) {
2062
+ clearPing();
2063
+ if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
2064
+ livenessRemountCountRef.current += 1;
2065
+ recoverNativeWebView();
2066
+ }
2067
+ } else {
2068
+ livenessRemountCountRef.current = 0;
2069
+ clearPing();
2070
+ }
2071
+ }
2072
+ return;
2073
+ }
2074
+ internalSagepilotChat.handleHostedBridgeMessage(message);
1383
2075
  };
1384
2076
  const postIdentityToWebFrame = () => {
1385
2077
  const message = internalSagepilotChat.getHostedIdentityMessage();
@@ -1392,8 +2084,25 @@ function SagepilotChatProvider({
1392
2084
  if (!script || !nativeWebViewRef.current) return;
1393
2085
  nativeWebViewRef.current.injectJavaScript(script);
1394
2086
  };
2087
+ const handleNativeWebViewLoadEnd = () => {
2088
+ postIdentityToNativeWebView();
2089
+ widgetReadyRef.current = false;
2090
+ startDelivery();
2091
+ };
2092
+ const handleShouldStartLoadWithRequest = useCallback((request) => {
2093
+ const url = request?.url ?? "";
2094
+ if (!url || request?.isTopFrame === false || isInternalWebViewScheme(url)) {
2095
+ return true;
2096
+ }
2097
+ const widgetOrigin = readUrlOrigin(state.conversationUrl ?? state.preloadUrl);
2098
+ if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
2099
+ return true;
2100
+ }
2101
+ Linking.openURL(url).catch(() => void 0);
2102
+ return false;
2103
+ }, [state.conversationUrl, state.preloadUrl]);
1395
2104
  useEffect(() => {
1396
- if (Platform.OS !== "web" || typeof window === "undefined") return;
2105
+ if (Platform2.OS !== "web" || typeof window === "undefined") return;
1397
2106
  const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
1398
2107
  if (!trustedWidgetOrigin) return;
1399
2108
  const handleWindowMessage = (event) => {
@@ -1404,14 +2113,14 @@ function SagepilotChatProvider({
1404
2113
  return () => window.removeEventListener("message", handleWindowMessage);
1405
2114
  }, [state.conversationUrl]);
1406
2115
  useEffect(() => {
1407
- if (Platform.OS !== "web" || !state.isPresented) return;
2116
+ if (Platform2.OS !== "web" || !state.isPresented) return;
1408
2117
  postIdentityToWebFrame();
1409
2118
  }, [state.isPresented, state.conversationUrl, state.configured]);
1410
2119
  useEffect(() => {
1411
- if (Platform.OS === "web" || !state.isPresented) return;
2120
+ if (Platform2.OS === "web" || !state.isPresented) return;
1412
2121
  postIdentityToNativeWebView();
1413
2122
  }, [state.isPresented, state.conversationUrl, state.configured]);
1414
- if (Platform.OS === "web") {
2123
+ if (Platform2.OS === "web") {
1415
2124
  return createElement(
1416
2125
  View,
1417
2126
  { style: styles.root },
@@ -1454,10 +2163,18 @@ function SagepilotChatProvider({
1454
2163
  children,
1455
2164
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? createElement(WebView, {
1456
2165
  ...hostedChatWebViewProps,
2166
+ // Separate key from the hosted WebView: a hidden-preload renderer crash
2167
+ // must not bump the hosted key (which would reset widgetReadyRef and
2168
+ // disrupt an in-flight delivery). onRenderProcessGone must still be
2169
+ // handled here or an unhandled renderer kill crashes the whole app.
2170
+ key: `sagepilot-preload-webview-${preloadWebViewKey}`,
1457
2171
  source: { uri: state.preloadUrl },
1458
2172
  style: styles.preloadWebview,
1459
2173
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1460
- onMessage: handleWebViewMessage
2174
+ onMessage: handleWebViewMessage,
2175
+ onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
2176
+ onRenderProcessGone: recoverPreloadWebView,
2177
+ onContentProcessDidTerminate: recoverPreloadWebView
1461
2178
  }) : null,
1462
2179
  createElement(
1463
2180
  Modal,
@@ -1465,8 +2182,8 @@ function SagepilotChatProvider({
1465
2182
  visible: state.configured && state.isPresented,
1466
2183
  animationType,
1467
2184
  presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
1468
- statusBarTranslucent: Platform.OS === "android",
1469
- navigationBarTranslucent: Platform.OS === "android",
2185
+ statusBarTranslucent: Platform2.OS === "android",
2186
+ navigationBarTranslucent: Platform2.OS === "android",
1470
2187
  onRequestClose: () => internalSagepilotChat.dismiss()
1471
2188
  },
1472
2189
  createElement(
@@ -1491,13 +2208,23 @@ function SagepilotChatProvider({
1491
2208
  ) : null,
1492
2209
  state.conversationUrl ? createElement(WebView, {
1493
2210
  ...hostedChatWebViewProps,
2211
+ key: `sagepilot-hosted-webview-${nativeWebViewKey}`,
1494
2212
  ref: nativeWebViewRef,
1495
2213
  source: { uri: state.conversationUrl },
1496
- style: styles.webview,
2214
+ // The imperceptible opacity toggle forces the Android WebView
2215
+ // surface to re-composite on resume, clearing the blank-but-alive
2216
+ // surface bug without a reload (see runLivenessProbe).
2217
+ style: Platform2.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
1497
2218
  startInLoadingState: true,
1498
2219
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1499
2220
  onMessage: handleWebViewMessage,
1500
- onLoadEnd: postIdentityToNativeWebView,
2221
+ onLoadEnd: handleNativeWebViewLoadEnd,
2222
+ onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
2223
+ // Android: render process killed (commonly while the camera/file
2224
+ // chooser activity is foregrounded). Remount to recover.
2225
+ onRenderProcessGone: recoverNativeWebView,
2226
+ // iOS equivalent: WKWebView content process terminated.
2227
+ onContentProcessDidTerminate: recoverNativeWebView,
1501
2228
  renderLoading: () => createElement(
1502
2229
  View,
1503
2230
  { style: styles.loading },
@@ -1507,6 +2234,48 @@ function SagepilotChatProvider({
1507
2234
  }) : null
1508
2235
  )
1509
2236
  )
2237
+ ),
2238
+ // Native attachment-source chooser. Replaces the Android Alert (capped at
2239
+ // 3 buttons) so camera/library/documents all show on every platform.
2240
+ createElement(
2241
+ Modal,
2242
+ {
2243
+ visible: sourceChooser !== null,
2244
+ transparent: true,
2245
+ animationType: "fade",
2246
+ statusBarTranslucent: true,
2247
+ onRequestClose: () => setSourceChooser(null)
2248
+ },
2249
+ createElement(
2250
+ Pressable,
2251
+ { style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
2252
+ createElement(
2253
+ View,
2254
+ { style: styles.sheetCard },
2255
+ createElement(Text, { style: styles.sheetTitle }, "Add attachment"),
2256
+ ...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
2257
+ (source) => createElement(
2258
+ Pressable,
2259
+ {
2260
+ key: source,
2261
+ accessibilityRole: "button",
2262
+ style: styles.sheetButton,
2263
+ onPress: () => handleSourceChoice(source)
2264
+ },
2265
+ createElement(Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source])
2266
+ )
2267
+ ),
2268
+ createElement(
2269
+ Pressable,
2270
+ {
2271
+ accessibilityRole: "button",
2272
+ style: [styles.sheetButton, styles.sheetCancelButton],
2273
+ onPress: () => setSourceChooser(null)
2274
+ },
2275
+ createElement(Text, { style: styles.sheetCancelText }, "Cancel")
2276
+ )
2277
+ )
2278
+ )
1510
2279
  )
1511
2280
  );
1512
2281
  }
@@ -1605,6 +2374,46 @@ var styles = StyleSheet.create({
1605
2374
  marginTop: 12,
1606
2375
  color: "#4b5563",
1607
2376
  fontSize: 14
2377
+ },
2378
+ sheetBackdrop: {
2379
+ flex: 1,
2380
+ justifyContent: "flex-end",
2381
+ backgroundColor: "rgba(17, 24, 39, 0.36)"
2382
+ },
2383
+ sheetCard: {
2384
+ backgroundColor: "#ffffff",
2385
+ borderTopLeftRadius: 16,
2386
+ borderTopRightRadius: 16,
2387
+ paddingTop: 8,
2388
+ paddingBottom: 24,
2389
+ paddingHorizontal: 8
2390
+ },
2391
+ sheetTitle: {
2392
+ textAlign: "center",
2393
+ color: "#6b7280",
2394
+ fontSize: 13,
2395
+ fontWeight: "600",
2396
+ paddingVertical: 10
2397
+ },
2398
+ sheetButton: {
2399
+ minHeight: 52,
2400
+ alignItems: "center",
2401
+ justifyContent: "center",
2402
+ borderRadius: 12
2403
+ },
2404
+ sheetButtonText: {
2405
+ color: "#111827",
2406
+ fontSize: 16,
2407
+ fontWeight: "500"
2408
+ },
2409
+ sheetCancelButton: {
2410
+ marginTop: 6,
2411
+ backgroundColor: "#f3f4f6"
2412
+ },
2413
+ sheetCancelText: {
2414
+ color: "#111827",
2415
+ fontSize: 16,
2416
+ fontWeight: "600"
1608
2417
  }
1609
2418
  });
1610
2419
 
@@ -1661,7 +2470,10 @@ export {
1661
2470
  SagepilotChat,
1662
2471
  SagepilotChatError,
1663
2472
  SagepilotChatProvider,
2473
+ SagepilotFilePickerError,
1664
2474
  createAsyncStorageCacheStorage,
1665
2475
  createKeychainTokenStorage,
2476
+ createSagepilotFilePicker,
2477
+ createSagepilotFileStore,
1666
2478
  useSagepilotChat
1667
2479
  };