@sagepilot-ai/react-native-sdk 0.2.3 → 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.3";
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";
@@ -505,6 +508,18 @@ function normalizeIdentity(identity) {
505
508
  user_hash: identity.userHash ?? identity.user_hash
506
509
  };
507
510
  }
511
+ function readStringField(input, key) {
512
+ const value = input[key];
513
+ return typeof value === "string" && value.trim() ? value : void 0;
514
+ }
515
+ function normalizeOptionalString(value) {
516
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
517
+ }
518
+ function readRecordField(input, key) {
519
+ const value = input[key];
520
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
521
+ return value;
522
+ }
508
523
  function toPublicSessionState(session) {
509
524
  return {
510
525
  session_id: session.session_id,
@@ -538,6 +553,7 @@ var SagepilotReactNativeChat = class {
538
553
  this.unreadPollTimer = null;
539
554
  this.stateCallbacks = /* @__PURE__ */ new Set();
540
555
  this.identifyCallbacks = /* @__PURE__ */ new Set();
556
+ this.conversationCreatedCallbacks = /* @__PURE__ */ new Set();
541
557
  this.unreadCallbacks = /* @__PURE__ */ new Set();
542
558
  this.readyCallbacks = /* @__PURE__ */ new Set();
543
559
  this.presentCallbacks = /* @__PURE__ */ new Set();
@@ -560,6 +576,9 @@ var SagepilotReactNativeChat = class {
560
576
  const { workspaceId, channelId } = parseKey(config.key);
561
577
  this.runtimeOptions = {
562
578
  behavior: config.behavior,
579
+ cacheStorage: config.cacheStorage,
580
+ filePicker: config.filePicker,
581
+ fileStore: config.fileStore,
563
582
  presentation: config.presentation,
564
583
  theme: config.theme,
565
584
  hostedClaimsStorageKeyPrefix: config.hostedClaimsStorageKeyPrefix || DEFAULT_HOSTED_CLAIMS_STORAGE_KEY_PREFIX
@@ -744,7 +763,15 @@ var SagepilotReactNativeChat = class {
744
763
  }
745
764
  presentMessageComposer(message, options) {
746
765
  this.requireConfigured();
747
- this.hostedChatView = { screen: "composer", message, mode: options?.mode ?? "auto" };
766
+ const chatId = normalizeOptionalString(options?.chatId);
767
+ this.hostedChatView = {
768
+ screen: "composer",
769
+ message,
770
+ mode: options?.mode ?? "auto",
771
+ chatId,
772
+ metadata: options?.metadata,
773
+ onConversationCreated: options?.onConversationCreated
774
+ };
748
775
  return this.showHostedChat();
749
776
  }
750
777
  dismiss() {
@@ -793,6 +820,13 @@ var SagepilotReactNativeChat = class {
793
820
  this.dismissCallbacks.delete(callback);
794
821
  };
795
822
  }
823
+ onConversationCreated(callback) {
824
+ if (typeof callback !== "function") return () => void 0;
825
+ this.conversationCreatedCallbacks.add(callback);
826
+ return () => {
827
+ this.conversationCreatedCallbacks.delete(callback);
828
+ };
829
+ }
796
830
  onError(callback) {
797
831
  if (typeof callback !== "function") return () => void 0;
798
832
  this.errorCallbacks.add(callback);
@@ -844,7 +878,10 @@ var SagepilotReactNativeChat = class {
844
878
  url.searchParams.set("jwt", this.legacyWidgetJwt);
845
879
  }
846
880
  if (this.hostedChatView.screen === "composer") {
847
- if (this.hostedChatView.mode === "new") {
881
+ if (this.hostedChatView.chatId) {
882
+ url.searchParams.set("chat_id", this.hostedChatView.chatId);
883
+ }
884
+ if (this.hostedChatView.mode === "new" && !this.hostedChatView.chatId) {
848
885
  url.searchParams.set("new", "true");
849
886
  }
850
887
  if (this.hostedChatView.message) {
@@ -872,13 +909,19 @@ var SagepilotReactNativeChat = class {
872
909
  "})();"
873
910
  ].join("\n");
874
911
  }
912
+ getActiveHostedChatId() {
913
+ if (this.hostedChatView.screen === "composer" && this.hostedChatView.chatId) {
914
+ return this.hostedChatView.chatId;
915
+ }
916
+ return this.session?.conversation_id ?? void 0;
917
+ }
875
918
  getHostedIdentityMessage() {
876
919
  if (!this.legacyWidgetJwt) return null;
877
920
  return {
878
921
  type: "identity_update",
879
922
  data: {
880
923
  jwt: this.legacyWidgetJwt,
881
- chat_id: this.session?.conversation_id ?? void 0
924
+ chat_id: this.getActiveHostedChatId()
882
925
  }
883
926
  };
884
927
  }
@@ -897,17 +940,62 @@ var SagepilotReactNativeChat = class {
897
940
  }
898
941
  return true;
899
942
  }
943
+ if (message.type === "sagepilot:conversation_created") {
944
+ this.handleConversationCreated(message);
945
+ return true;
946
+ }
900
947
  if (message.type === "sagepilot:error") {
901
948
  this.emitError(message);
902
949
  return true;
903
950
  }
904
951
  return true;
905
952
  }
953
+ handleConversationCreated(message) {
954
+ const chatId = readStringField(message, "chat_id") ?? readStringField(message, "conversation_id");
955
+ if (!chatId) return;
956
+ void this.persistConversationId(chatId).catch((error) => this.emitError(error));
957
+ const bridgeMetadata = readRecordField(message, "metadata");
958
+ const composerMetadata = this.hostedChatView.screen === "composer" ? this.hostedChatView.metadata : void 0;
959
+ const metadata = {
960
+ ...composerMetadata ?? {},
961
+ ...bridgeMetadata ?? {}
962
+ };
963
+ const workspaceId = readStringField(message, "workspace_id") ?? this.workspaceId;
964
+ const channelId = readStringField(message, "channel_id") ?? this.channelId;
965
+ const sessionId = readStringField(message, "session_id") ?? this.session?.session_id;
966
+ const customerId = readStringField(message, "customer_id");
967
+ const messageId = readStringField(message, "message_id");
968
+ const createdAt = readStringField(message, "created_at");
969
+ const event = {
970
+ chat_id: chatId,
971
+ ...workspaceId ? { workspace_id: workspaceId } : {},
972
+ ...channelId ? { channel_id: channelId } : {},
973
+ ...sessionId ? { session_id: sessionId } : {},
974
+ ...customerId ? { customer_id: customerId } : {},
975
+ ...messageId ? { message_id: messageId } : {},
976
+ ...createdAt ? { created_at: createdAt } : {},
977
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
978
+ };
979
+ if (this.hostedChatView.screen === "composer") {
980
+ this.hostedChatView.onConversationCreated?.(event);
981
+ }
982
+ this.conversationCreatedCallbacks.forEach((callback) => callback(event));
983
+ }
984
+ async persistConversationId(conversationId) {
985
+ if (!this.session || !this.sessionManager) return;
986
+ if (this.session.conversation_id === conversationId) return;
987
+ this.session = await this.sessionManager.setSession({
988
+ ...this.session,
989
+ conversation_id: conversationId
990
+ });
991
+ this.emitState();
992
+ }
906
993
  destroy() {
907
994
  this.stopUnreadPolling();
908
995
  this.resetRuntimeState();
909
996
  this.emitState();
910
997
  this.identifyCallbacks.clear();
998
+ this.conversationCreatedCallbacks.clear();
911
999
  this.unreadCallbacks.clear();
912
1000
  this.readyCallbacks.clear();
913
1001
  this.presentCallbacks.clear();
@@ -1070,6 +1158,9 @@ var SagepilotChat = {
1070
1158
  logout: () => internalSagepilotChat.logout(),
1071
1159
  getIdentityState: () => internalSagepilotChat.getIdentityState(),
1072
1160
  onIdentify: (callback) => internalSagepilotChat.onIdentify(callback),
1161
+ onConversationCreated: (callback) => {
1162
+ return internalSagepilotChat.onConversationCreated(callback);
1163
+ },
1073
1164
  getUnreadCount: () => internalSagepilotChat.getUnreadCount(),
1074
1165
  onUnreadChange: (callback) => internalSagepilotChat.onUnreadChange(callback),
1075
1166
  startUnreadPolling: (intervalMs) => internalSagepilotChat.startUnreadPolling(intervalMs),
@@ -1102,13 +1193,323 @@ function createAsyncStorageCacheStorage(asyncStorage) {
1102
1193
  removeItem: (key) => asyncStorage.removeItem(key)
1103
1194
  };
1104
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
+ }
1105
1475
 
1106
1476
  // src/ui/SagepilotChatProvider.ts
1107
1477
  var import_react = require("react");
1108
- var import_react_native = require("react-native");
1478
+ var import_react_native2 = require("react-native");
1109
1479
  var import_react_native_webview = require("react-native-webview");
1110
1480
 
1111
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
+ }
1112
1513
  function isHostedBridgeMessage(value) {
1113
1514
  if (!value || typeof value !== "object") return false;
1114
1515
  const message = value;
@@ -1234,9 +1635,70 @@ function readPresentationState() {
1234
1635
  function getInjectedWebViewScript() {
1235
1636
  return [
1236
1637
  internalSagepilotChat.getHostedAuthScript(),
1237
- mobileWebViewBridgeScript
1638
+ mobileWebViewBridgeScript,
1639
+ buildBridgeCapabilitiesScript({
1640
+ nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1641
+ })
1238
1642
  ].filter(Boolean).join("\n");
1239
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
+ }
1240
1702
  function getHostedIdentityDispatchScript() {
1241
1703
  const message = internalSagepilotChat.getHostedIdentityMessage();
1242
1704
  if (!message) return "";
@@ -1259,19 +1721,28 @@ function readUrlOrigin(url) {
1259
1721
  return null;
1260
1722
  }
1261
1723
  }
1724
+ function isInternalWebViewScheme(url) {
1725
+ return url.startsWith("about:") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("javascript:") || url.startsWith("file:");
1726
+ }
1262
1727
  var hostedChatWebViewProps = {
1263
1728
  allowFileAccess: true,
1264
1729
  allowFileAccessFromFileURLs: true,
1730
+ ...import_react_native2.Platform.OS === "ios" ? {
1731
+ automaticallyAdjustContentInsets: false,
1732
+ contentInsetAdjustmentBehavior: "never",
1733
+ hideKeyboardAccessoryView: true
1734
+ } : null,
1265
1735
  bounces: false,
1266
1736
  domStorageEnabled: true,
1267
1737
  javaScriptEnabled: true,
1268
1738
  overScrollMode: "never",
1269
- scrollEnabled: import_react_native.Platform.OS !== "android",
1739
+ // The hosted widget owns feed scrolling internally; outer WebView scrolling lets iOS focus-scroll the page over the keyboard.
1740
+ scrollEnabled: false,
1270
1741
  setSupportMultipleWindows: false,
1271
1742
  sharedCookiesEnabled: true,
1272
1743
  thirdPartyCookiesEnabled: true
1273
1744
  };
1274
- 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;
1275
1746
  var emptyAndroidInsets = { top: 0, bottom: 0 };
1276
1747
  function SagepilotChatProvider({
1277
1748
  children,
@@ -1280,13 +1751,292 @@ function SagepilotChatProvider({
1280
1751
  }) {
1281
1752
  const [state, setState] = (0, import_react.useState)(readPresentationState);
1282
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);
1283
1756
  const webFrameRef = (0, import_react.useRef)(null);
1284
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]);
1285
1923
  (0, import_react.useEffect)(() => {
1286
1924
  return internalSagepilotChat.onStateChange(() => {
1287
1925
  setState(readPresentationState());
1288
1926
  });
1289
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]);
1290
2040
  const handleAndroidInsetsChange = (0, import_react.useCallback)((event) => {
1291
2041
  const nextBottomInset = event.nativeEvent?.bottom;
1292
2042
  const nextTopInset = event.nativeEvent?.top;
@@ -1300,11 +2050,11 @@ function SagepilotChatProvider({
1300
2050
  const presentationStyle = state.presentation?.style ?? "sheet";
1301
2051
  const isFullScreenModal = presentationStyle === "fullScreen";
1302
2052
  const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
1303
- const ModalContainer = import_react_native.Platform.OS === "ios" && isFullScreenModal ? import_react_native.SafeAreaView : import_react_native.View;
1304
- const NativeModalContainer = import_react_native.Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
1305
- const ChatContentContainer = import_react_native.Platform.OS === "ios" ? import_react_native.KeyboardAvoidingView : import_react_native.View;
1306
- const nativeModalContainerProps = import_react_native.Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
1307
- 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" ? {
1308
2058
  behavior: "padding",
1309
2059
  enabled: true,
1310
2060
  keyboardVerticalOffset: 0,
@@ -1312,14 +2062,48 @@ function SagepilotChatProvider({
1312
2062
  } : {
1313
2063
  style: [
1314
2064
  styles.modalContent,
1315
- import_react_native.Platform.OS === "android" ? {
2065
+ import_react_native2.Platform.OS === "android" ? {
1316
2066
  paddingTop: androidModalInsets.top,
1317
2067
  paddingBottom: androidModalInsets.bottom
1318
2068
  } : null
1319
2069
  ].filter(Boolean)
1320
2070
  };
1321
2071
  const handleWebViewMessage = (event) => {
1322
- 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);
1323
2107
  };
1324
2108
  const postIdentityToWebFrame = () => {
1325
2109
  const message = internalSagepilotChat.getHostedIdentityMessage();
@@ -1332,8 +2116,25 @@ function SagepilotChatProvider({
1332
2116
  if (!script || !nativeWebViewRef.current) return;
1333
2117
  nativeWebViewRef.current.injectJavaScript(script);
1334
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]);
1335
2136
  (0, import_react.useEffect)(() => {
1336
- if (import_react_native.Platform.OS !== "web" || typeof window === "undefined") return;
2137
+ if (import_react_native2.Platform.OS !== "web" || typeof window === "undefined") return;
1337
2138
  const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
1338
2139
  if (!trustedWidgetOrigin) return;
1339
2140
  const handleWindowMessage = (event) => {
@@ -1344,16 +2145,16 @@ function SagepilotChatProvider({
1344
2145
  return () => window.removeEventListener("message", handleWindowMessage);
1345
2146
  }, [state.conversationUrl]);
1346
2147
  (0, import_react.useEffect)(() => {
1347
- if (import_react_native.Platform.OS !== "web" || !state.isPresented) return;
2148
+ if (import_react_native2.Platform.OS !== "web" || !state.isPresented) return;
1348
2149
  postIdentityToWebFrame();
1349
2150
  }, [state.isPresented, state.conversationUrl, state.configured]);
1350
2151
  (0, import_react.useEffect)(() => {
1351
- if (import_react_native.Platform.OS === "web" || !state.isPresented) return;
2152
+ if (import_react_native2.Platform.OS === "web" || !state.isPresented) return;
1352
2153
  postIdentityToNativeWebView();
1353
2154
  }, [state.isPresented, state.conversationUrl, state.configured]);
1354
- if (import_react_native.Platform.OS === "web") {
2155
+ if (import_react_native2.Platform.OS === "web") {
1355
2156
  return (0, import_react.createElement)(
1356
- import_react_native.View,
2157
+ import_react_native2.View,
1357
2158
  { style: styles.root },
1358
2159
  children,
1359
2160
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)("iframe", {
@@ -1362,20 +2163,20 @@ function SagepilotChatProvider({
1362
2163
  title: "Sagepilot chat preload"
1363
2164
  }) : null,
1364
2165
  state.configured && state.isPresented && state.conversationUrl ? (0, import_react.createElement)(
1365
- import_react_native.View,
2166
+ import_react_native2.View,
1366
2167
  { style: styles.webOverlay },
1367
2168
  (0, import_react.createElement)(
1368
- import_react_native.View,
2169
+ import_react_native2.View,
1369
2170
  { style: styles.webPanel },
1370
2171
  showCloseButton ? (0, import_react.createElement)(
1371
- import_react_native.Pressable,
2172
+ import_react_native2.Pressable,
1372
2173
  {
1373
2174
  accessibilityRole: "button",
1374
2175
  accessibilityLabel: closeLabel,
1375
2176
  onPress: () => internalSagepilotChat.dismiss(),
1376
2177
  style: styles.webCloseButton
1377
2178
  },
1378
- (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)
1379
2180
  ) : null,
1380
2181
  (0, import_react.createElement)("iframe", {
1381
2182
  ref: webFrameRef,
@@ -1389,24 +2190,32 @@ function SagepilotChatProvider({
1389
2190
  );
1390
2191
  }
1391
2192
  return (0, import_react.createElement)(
1392
- import_react_native.View,
2193
+ import_react_native2.View,
1393
2194
  { style: styles.root },
1394
2195
  children,
1395
2196
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
1396
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}`,
1397
2203
  source: { uri: state.preloadUrl },
1398
2204
  style: styles.preloadWebview,
1399
2205
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1400
- onMessage: handleWebViewMessage
2206
+ onMessage: handleWebViewMessage,
2207
+ onShouldStartLoadWithRequest: handleShouldStartLoadWithRequest,
2208
+ onRenderProcessGone: recoverPreloadWebView,
2209
+ onContentProcessDidTerminate: recoverPreloadWebView
1401
2210
  }) : null,
1402
2211
  (0, import_react.createElement)(
1403
- import_react_native.Modal,
2212
+ import_react_native2.Modal,
1404
2213
  {
1405
2214
  visible: state.configured && state.isPresented,
1406
2215
  animationType,
1407
2216
  presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
1408
- statusBarTranslucent: import_react_native.Platform.OS === "android",
1409
- navigationBarTranslucent: import_react_native.Platform.OS === "android",
2217
+ statusBarTranslucent: import_react_native2.Platform.OS === "android",
2218
+ navigationBarTranslucent: import_react_native2.Platform.OS === "android",
1410
2219
  onRequestClose: () => internalSagepilotChat.dismiss()
1411
2220
  },
1412
2221
  (0, import_react.createElement)(
@@ -1416,41 +2225,93 @@ function SagepilotChatProvider({
1416
2225
  ChatContentContainer,
1417
2226
  chatContentContainerProps,
1418
2227
  showCloseButton ? (0, import_react.createElement)(
1419
- import_react_native.View,
2228
+ import_react_native2.View,
1420
2229
  { style: styles.header },
1421
2230
  (0, import_react.createElement)(
1422
- import_react_native.Pressable,
2231
+ import_react_native2.Pressable,
1423
2232
  {
1424
2233
  accessibilityRole: "button",
1425
2234
  accessibilityLabel: closeLabel,
1426
2235
  onPress: () => internalSagepilotChat.dismiss(),
1427
2236
  style: styles.closeButton
1428
2237
  },
1429
- (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)
1430
2239
  )
1431
2240
  ) : null,
1432
2241
  state.conversationUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
1433
2242
  ...hostedChatWebViewProps,
2243
+ key: `sagepilot-hosted-webview-${nativeWebViewKey}`,
1434
2244
  ref: nativeWebViewRef,
1435
2245
  source: { uri: state.conversationUrl },
1436
- 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,
1437
2250
  startInLoadingState: true,
1438
2251
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
1439
2252
  onMessage: handleWebViewMessage,
1440
- 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,
1441
2260
  renderLoading: () => (0, import_react.createElement)(
1442
- import_react_native.View,
2261
+ import_react_native2.View,
1443
2262
  { style: styles.loading },
1444
- (0, import_react.createElement)(import_react_native.ActivityIndicator, null),
1445
- (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)
1446
2265
  )
1447
2266
  }) : null
1448
2267
  )
1449
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
+ )
1450
2311
  )
1451
2312
  );
1452
2313
  }
1453
- var styles = import_react_native.StyleSheet.create({
2314
+ var styles = import_react_native2.StyleSheet.create({
1454
2315
  root: {
1455
2316
  flex: 1
1456
2317
  },
@@ -1536,7 +2397,7 @@ var styles = import_react_native.StyleSheet.create({
1536
2397
  borderStyle: "none"
1537
2398
  },
1538
2399
  loading: {
1539
- ...import_react_native.StyleSheet.absoluteFillObject,
2400
+ ...import_react_native2.StyleSheet.absoluteFillObject,
1540
2401
  alignItems: "center",
1541
2402
  justifyContent: "center",
1542
2403
  backgroundColor: "#ffffff"
@@ -1545,6 +2406,46 @@ var styles = import_react_native.StyleSheet.create({
1545
2406
  marginTop: 12,
1546
2407
  color: "#4b5563",
1547
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"
1548
2449
  }
1549
2450
  });
1550
2451
 
@@ -1580,6 +2481,9 @@ function useSagepilotChat() {
1580
2481
  }, []);
1581
2482
  const logout = (0, import_react2.useCallback)(() => SagepilotChat.logout(), []);
1582
2483
  const getUnreadCount = (0, import_react2.useCallback)(() => SagepilotChat.getUnreadCount(), []);
2484
+ const onConversationCreated = (0, import_react2.useCallback)((callback) => {
2485
+ return SagepilotChat.onConversationCreated(callback);
2486
+ }, []);
1583
2487
  return {
1584
2488
  ...state,
1585
2489
  present,
@@ -1590,7 +2494,8 @@ function useSagepilotChat() {
1590
2494
  toggle,
1591
2495
  identify,
1592
2496
  logout,
1593
- getUnreadCount
2497
+ getUnreadCount,
2498
+ onConversationCreated
1594
2499
  };
1595
2500
  }
1596
2501
  // Annotate the CommonJS export names for ESM import in node:
@@ -1598,7 +2503,10 @@ function useSagepilotChat() {
1598
2503
  SagepilotChat,
1599
2504
  SagepilotChatError,
1600
2505
  SagepilotChatProvider,
2506
+ SagepilotFilePickerError,
1601
2507
  createAsyncStorageCacheStorage,
1602
2508
  createKeychainTokenStorage,
2509
+ createSagepilotFilePicker,
2510
+ createSagepilotFileStore,
1603
2511
  useSagepilotChat
1604
2512
  });