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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,6 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION: () => SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
34
+ SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES: () => SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
35
+ SAGEPILOT_DEFAULT_IMAGE_QUALITY: () => SAGEPILOT_DEFAULT_IMAGE_QUALITY,
33
36
  SagepilotChat: () => SagepilotChat,
34
37
  SagepilotChatError: () => SagepilotChatError,
35
38
  SagepilotChatProvider: () => SagepilotChatProvider,
@@ -38,10 +41,15 @@ __export(index_exports, {
38
41
  createKeychainTokenStorage: () => createKeychainTokenStorage,
39
42
  createSagepilotFilePicker: () => createSagepilotFilePicker,
40
43
  createSagepilotFileStore: () => createSagepilotFileStore,
44
+ ensureSagepilotAndroidCameraPermission: () => ensureSagepilotAndroidCameraPermission,
45
+ promptOpenSagepilotCameraSettings: () => promptOpenSagepilotCameraSettings,
41
46
  useSagepilotChat: () => useSagepilotChat
42
47
  });
43
48
  module.exports = __toCommonJS(index_exports);
44
49
 
50
+ // src/public/SdkClient.ts
51
+ var import_react_native = require("react-native");
52
+
45
53
  // src/core/errors/SagepilotChatError.ts
46
54
  var SagepilotChatError = class extends Error {
47
55
  constructor(code, message, options = {}) {
@@ -59,7 +67,7 @@ var SagepilotChatError = class extends Error {
59
67
 
60
68
  // src/core/config/constants.ts
61
69
  var SDK_NAME = "@sagepilot-ai/react-native-sdk";
62
- var SDK_VERSION = "0.2.5";
70
+ var SDK_VERSION = "0.3.0";
63
71
  var DEFAULT_HOST = "https://app.sagepilot.ai";
64
72
  var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
65
73
  var CUSTOMER_API_PREFIX = "/customer-api/v1";
@@ -368,6 +376,34 @@ async function resolveDeviceInfo(adapter) {
368
376
  return adapter.getDeviceInfo();
369
377
  }
370
378
 
379
+ // src/core/storage/cache.ts
380
+ function createAsyncStorageCacheStorage(asyncStorage) {
381
+ return {
382
+ getItem: (key) => asyncStorage.getItem(key),
383
+ setItem: (key, value) => asyncStorage.setItem(key, value),
384
+ removeItem: (key) => asyncStorage.removeItem(key)
385
+ };
386
+ }
387
+ function createJsonCache(storage, namespace) {
388
+ return {
389
+ async get(key) {
390
+ const value = await storage.getItem(`${namespace}:${key}`);
391
+ if (!value) return null;
392
+ try {
393
+ return JSON.parse(value);
394
+ } catch {
395
+ return null;
396
+ }
397
+ },
398
+ async set(key, value) {
399
+ await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
400
+ },
401
+ async remove(key) {
402
+ await storage.removeItem(`${namespace}:${key}`);
403
+ }
404
+ };
405
+ }
406
+
371
407
  // src/resources/channels.ts
372
408
  function bootstrapChannel(http, host, channelId) {
373
409
  return http.request(
@@ -451,6 +487,9 @@ function fetchUnreadCount(http, host, channelId, sessionId, authorizationHeader)
451
487
  }
452
488
 
453
489
  // src/public/SdkClient.ts
490
+ var PRESENTATION_CACHE_NAMESPACE = "sagepilot_rn_presentation";
491
+ var PRESENTATION_CACHE_KEY = "state";
492
+ var PRESENTATION_RESTORE_MAX_AGE_MS = 15 * 60 * 1e3;
454
493
  var DEFAULT_MOBILE_LAUNCHER_CONFIG = {
455
494
  label: "Chat",
456
495
  buttonColor: "#173c2d",
@@ -520,6 +559,10 @@ function readRecordField(input, key) {
520
559
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
521
560
  return value;
522
561
  }
562
+ function isFreshPresentationState(updatedAt) {
563
+ if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) return false;
564
+ return Date.now() - updatedAt <= PRESENTATION_RESTORE_MAX_AGE_MS;
565
+ }
523
566
  function toPublicSessionState(session) {
524
567
  return {
525
568
  session_id: session.session_id,
@@ -609,6 +652,7 @@ var SagepilotReactNativeChat = class {
609
652
  this.channel = channel;
610
653
  this.session = session;
611
654
  this.isConfigured = true;
655
+ await this.restorePresentationFromCache();
612
656
  this.emitReady();
613
657
  this.emitState();
614
658
  if (config.behavior?.enableUnreadPolling ?? true) {
@@ -748,12 +792,14 @@ var SagepilotReactNativeChat = class {
748
792
  this.hostedChatView = { screen: "home" };
749
793
  if (this.presented) {
750
794
  this.emitState();
795
+ this.persistPresentationState();
751
796
  return true;
752
797
  }
753
798
  this.presented = true;
754
799
  this.setUnreadCount(0);
755
800
  this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
756
801
  this.emitState();
802
+ this.persistPresentationState();
757
803
  return true;
758
804
  }
759
805
  presentMessages() {
@@ -781,6 +827,7 @@ var SagepilotReactNativeChat = class {
781
827
  this.emitUnreadChange();
782
828
  this.dismissCallbacks.forEach((callback) => callback(this.getLifecycleState()));
783
829
  this.emitState();
830
+ this.persistPresentationState();
784
831
  return true;
785
832
  }
786
833
  hide() {
@@ -915,6 +962,10 @@ var SagepilotReactNativeChat = class {
915
962
  }
916
963
  return this.session?.conversation_id ?? void 0;
917
964
  }
965
+ /** Returns the chat id that is explicitly part of the current hosted URL. */
966
+ getHostedRouteChatId() {
967
+ return this.hostedChatView.screen === "composer" ? this.hostedChatView.chatId : void 0;
968
+ }
918
969
  getHostedIdentityMessage() {
919
970
  if (!this.legacyWidgetJwt) return null;
920
971
  return {
@@ -925,6 +976,34 @@ var SagepilotReactNativeChat = class {
925
976
  }
926
977
  };
927
978
  }
979
+ /** Captures the hosted route that should receive a picked attachment batch. */
980
+ getHostedAttachmentTarget() {
981
+ return {
982
+ chatId: this.getActiveHostedChatId(),
983
+ routeChatId: this.getHostedRouteChatId(),
984
+ hostedChatView: this.serializeHostedChatView(this.hostedChatView)
985
+ };
986
+ }
987
+ /** Restores the hosted route for a queued attachment batch before delivery. */
988
+ restoreHostedAttachmentTarget(target) {
989
+ if (import_react_native.Platform.OS !== "android") return false;
990
+ if (!target) return false;
991
+ const hostedChatView = this.readPersistedHostedChatView(target.hostedChatView);
992
+ if (!hostedChatView) return false;
993
+ const currentChatId = this.getActiveHostedChatId();
994
+ const targetChatId = normalizeOptionalString(target.chatId);
995
+ const routeChatId = normalizeOptionalString(target.routeChatId) ?? (hostedChatView.screen === "composer" ? normalizeOptionalString(hostedChatView.chatId) : void 0);
996
+ const nextHostedChatView = routeChatId && hostedChatView.screen === "composer" ? this.withPinnedChatId(hostedChatView, routeChatId) : hostedChatView;
997
+ if (this.presented && (!targetChatId || currentChatId === targetChatId) && this.areHostedChatViewsEqual(this.hostedChatView, nextHostedChatView)) {
998
+ return false;
999
+ }
1000
+ this.hostedChatView = nextHostedChatView;
1001
+ this.presented = true;
1002
+ this.setUnreadCount(0);
1003
+ this.emitState();
1004
+ this.persistPresentationState();
1005
+ return true;
1006
+ }
928
1007
  handleHostedBridgeMessage(message) {
929
1008
  if (!message) return false;
930
1009
  if (message.type === "close_widget") {
@@ -992,6 +1071,7 @@ var SagepilotReactNativeChat = class {
992
1071
  }
993
1072
  destroy() {
994
1073
  this.stopUnreadPolling();
1074
+ this.persistPresentationState({ cleanShutdown: true, presented: false });
995
1075
  this.resetRuntimeState();
996
1076
  this.emitState();
997
1077
  this.identifyCallbacks.clear();
@@ -1147,8 +1227,86 @@ var SagepilotReactNativeChat = class {
1147
1227
  this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
1148
1228
  }
1149
1229
  this.emitState();
1230
+ this.persistPresentationState();
1150
1231
  return true;
1151
1232
  }
1233
+ /** Returns the dedicated cache used for hosted-chat presentation recovery. */
1234
+ getPresentationCache() {
1235
+ const cacheStorage = this.runtimeOptions?.cacheStorage;
1236
+ if (!cacheStorage) return null;
1237
+ return createJsonCache(cacheStorage, PRESENTATION_CACHE_NAMESPACE);
1238
+ }
1239
+ /** Removes callback-only fields so the hosted route can be safely serialized. */
1240
+ serializeHostedChatView(view) {
1241
+ if (view.screen !== "composer") return view;
1242
+ return {
1243
+ screen: "composer",
1244
+ message: view.message,
1245
+ mode: view.mode,
1246
+ chatId: view.chatId,
1247
+ metadata: view.metadata
1248
+ };
1249
+ }
1250
+ /** Validates a persisted hosted view before using it to rebuild a widget URL. */
1251
+ readPersistedHostedChatView(view) {
1252
+ if (!view || typeof view !== "object") return null;
1253
+ const record = view;
1254
+ if (record.screen === "home") return { screen: "home" };
1255
+ if (record.screen === "messages") return { screen: "messages" };
1256
+ if (record.screen !== "composer") return null;
1257
+ const mode = record.mode === "new" ? "new" : "auto";
1258
+ return {
1259
+ screen: "composer",
1260
+ message: typeof record.message === "string" ? record.message : void 0,
1261
+ mode,
1262
+ chatId: typeof record.chatId === "string" && record.chatId ? record.chatId : void 0,
1263
+ metadata: readRecordField(record, "metadata")
1264
+ };
1265
+ }
1266
+ /** Forces a persisted hosted route to target the exact chat that owns a pending attachment. */
1267
+ withPinnedChatId(view, chatId) {
1268
+ if (view.screen === "composer") {
1269
+ return {
1270
+ ...view,
1271
+ chatId
1272
+ };
1273
+ }
1274
+ return {
1275
+ screen: "composer",
1276
+ mode: "auto",
1277
+ chatId
1278
+ };
1279
+ }
1280
+ /** Compares hosted route state without relying on object identity. */
1281
+ areHostedChatViewsEqual(first, second) {
1282
+ return JSON.stringify(this.serializeHostedChatView(first)) === JSON.stringify(this.serializeHostedChatView(second));
1283
+ }
1284
+ /** Persists presentation state without blocking customer-facing SDK methods. */
1285
+ persistPresentationState(options) {
1286
+ const cache = this.getPresentationCache();
1287
+ if (!cache) return;
1288
+ const state = {
1289
+ presented: options?.presented ?? this.presented,
1290
+ hostedChatView: this.serializeHostedChatView(this.hostedChatView),
1291
+ updatedAt: Date.now(),
1292
+ cleanShutdown: options?.cleanShutdown
1293
+ };
1294
+ void cache.set(PRESENTATION_CACHE_KEY, state).catch(() => void 0);
1295
+ }
1296
+ /** Restores Android presentation state after configure without resetting the hosted route to Home. */
1297
+ async restorePresentationFromCache() {
1298
+ const cache = this.getPresentationCache();
1299
+ if (!cache || import_react_native.Platform.OS !== "android") return;
1300
+ const persisted = await cache.get(PRESENTATION_CACHE_KEY).catch(() => null);
1301
+ if (persisted?.cleanShutdown) return;
1302
+ if (!isFreshPresentationState(persisted?.updatedAt)) return;
1303
+ if (!persisted?.presented) return;
1304
+ const hostedChatView = this.readPersistedHostedChatView(persisted.hostedChatView);
1305
+ if (!hostedChatView) return;
1306
+ this.hostedChatView = hostedChatView;
1307
+ this.presented = true;
1308
+ this.setUnreadCount(0);
1309
+ }
1152
1310
  };
1153
1311
  var internalSagepilotChat = new SagepilotReactNativeChat();
1154
1312
  var SagepilotChat = {
@@ -1185,34 +1343,6 @@ var SagepilotChat = {
1185
1343
  destroy: () => internalSagepilotChat.destroy()
1186
1344
  };
1187
1345
 
1188
- // src/core/storage/cache.ts
1189
- function createAsyncStorageCacheStorage(asyncStorage) {
1190
- return {
1191
- getItem: (key) => asyncStorage.getItem(key),
1192
- setItem: (key, value) => asyncStorage.setItem(key, value),
1193
- removeItem: (key) => asyncStorage.removeItem(key)
1194
- };
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
1346
  // src/core/storage/fileStore.ts
1217
1347
  var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
1218
1348
  function sanitizeKey(key) {
@@ -1270,21 +1400,30 @@ function createSagepilotFileStore(blobUtil, options = {}) {
1270
1400
  }
1271
1401
 
1272
1402
  // 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;
1403
+ var import_react_native2 = require("react-native");
1404
+
1405
+ // src/core/native/filePickerShared.ts
1281
1406
  var SagepilotFilePickerError = class extends Error {
1407
+ /** Creates a typed picker error that can be surfaced over the widget bridge. */
1282
1408
  constructor(code, message) {
1283
1409
  super(message);
1284
1410
  this.name = "SagepilotFilePickerError";
1285
1411
  this.code = code;
1286
1412
  }
1287
1413
  };
1414
+ var SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION = 1920;
1415
+ var SAGEPILOT_DEFAULT_IMAGE_QUALITY = 0.8;
1416
+ var SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
1417
+
1418
+ // src/core/native/filePicker.ts
1419
+ var DEBUG_PREFIX = "[SagepilotSDK][FilePicker]";
1420
+ var DEFAULT_IMAGE_MAX_DIMENSION = SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION;
1421
+ var DEFAULT_IMAGE_QUALITY = SAGEPILOT_DEFAULT_IMAGE_QUALITY;
1422
+ var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
1423
+ var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
1424
+ var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
1425
+ var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1426
+ var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1288
1427
  function bytesToMb(bytes) {
1289
1428
  return Math.round(bytes / (1024 * 1024) * 10) / 10;
1290
1429
  }
@@ -1294,27 +1433,59 @@ function estimateBase64ByteSize(dataBase64) {
1294
1433
  function stripFileScheme(uri) {
1295
1434
  return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
1296
1435
  }
1297
- async function ensureAndroidCameraPermission() {
1298
- if (import_react_native.Platform.OS !== "android") return;
1436
+ function debugFilePicker(message, details) {
1437
+ console.log(`${DEBUG_PREFIX} ${message}`, details ?? "");
1438
+ }
1439
+ async function promptOpenSagepilotCameraSettings() {
1440
+ if (import_react_native2.Platform.OS !== "android") return;
1441
+ await new Promise((resolve) => {
1442
+ import_react_native2.Alert.alert(
1443
+ "Camera access is off",
1444
+ "Enable camera access for this app in Settings to take a photo.",
1445
+ [
1446
+ {
1447
+ text: "Not now",
1448
+ style: "cancel",
1449
+ onPress: resolve
1450
+ },
1451
+ {
1452
+ text: "Open Settings",
1453
+ onPress: () => {
1454
+ import_react_native2.Linking.openSettings().finally(resolve);
1455
+ }
1456
+ }
1457
+ ]
1458
+ );
1459
+ });
1460
+ }
1461
+ async function ensureSagepilotAndroidCameraPermission() {
1462
+ if (import_react_native2.Platform.OS !== "android") return;
1299
1463
  let result;
1300
1464
  try {
1301
- result = await import_react_native.PermissionsAndroid.request(import_react_native.PermissionsAndroid.PERMISSIONS.CAMERA);
1465
+ debugFilePicker("requesting Android camera permission");
1466
+ result = await import_react_native2.PermissionsAndroid.request(import_react_native2.PermissionsAndroid.PERMISSIONS.CAMERA);
1302
1467
  } catch {
1468
+ debugFilePicker("camera permission request threw; continuing to native launch");
1303
1469
  return;
1304
1470
  }
1305
- if (result === import_react_native.PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
1471
+ debugFilePicker("Android camera permission result", { result });
1472
+ if (result === import_react_native2.PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
1473
+ await promptOpenSagepilotCameraSettings();
1306
1474
  throw new SagepilotFilePickerError(
1307
1475
  "permission_denied",
1308
1476
  "Camera access is turned off. Enable it for this app in Settings to take a photo."
1309
1477
  );
1310
1478
  }
1311
- if (result === import_react_native.PermissionsAndroid.RESULTS.DENIED) {
1479
+ if (result === import_react_native2.PermissionsAndroid.RESULTS.DENIED) {
1312
1480
  throw new SagepilotFilePickerError(
1313
1481
  "permission_denied",
1314
1482
  "Camera permission is required to take a photo."
1315
1483
  );
1316
1484
  }
1317
1485
  }
1486
+ function canUseCameraSource(imagePicker) {
1487
+ return Boolean(imagePicker);
1488
+ }
1318
1489
  function mapImagePickerErrorCode(errorCode) {
1319
1490
  switch (errorCode) {
1320
1491
  case "permission":
@@ -1391,8 +1562,16 @@ function createSagepilotFilePicker(options) {
1391
1562
  const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
1392
1563
  const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1393
1564
  const sources = [];
1394
- if (imagePicker) sources.push("camera", "library");
1565
+ if (canUseCameraSource(imagePicker)) sources.push("camera");
1566
+ if (imagePicker) sources.push("library");
1395
1567
  if (documentsPicker) sources.push("documents");
1568
+ debugFilePicker("adapter created", {
1569
+ platform: import_react_native2.Platform.OS,
1570
+ hasImagePicker: Boolean(imagePicker),
1571
+ hasDocumentsPicker: Boolean(documentsPicker),
1572
+ hasFileReader: Boolean(fileReader),
1573
+ sources
1574
+ });
1396
1575
  if (sources.length === 0) return void 0;
1397
1576
  const imageOptions = {
1398
1577
  mediaType: "photo",
@@ -1403,12 +1582,22 @@ function createSagepilotFilePicker(options) {
1403
1582
  saveToPhotos: false
1404
1583
  };
1405
1584
  async function pickFromCamera() {
1406
- if (!imagePicker) return [];
1407
- await ensureAndroidCameraPermission();
1585
+ debugFilePicker("camera pick requested", {
1586
+ platform: import_react_native2.Platform.OS,
1587
+ imageMaxDimension,
1588
+ imageQuality,
1589
+ imageMaxFileSizeBytes
1590
+ });
1591
+ await ensureSagepilotAndroidCameraPermission();
1592
+ if (!imagePicker) {
1593
+ throw new SagepilotFilePickerError("camera_unavailable", "The camera picker is not available in this build.");
1594
+ }
1595
+ debugFilePicker("launching image-picker camera", { platform: import_react_native2.Platform.OS });
1408
1596
  return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
1409
1597
  }
1410
1598
  async function pickFromLibrary(multiple) {
1411
1599
  if (!imagePicker) return [];
1600
+ debugFilePicker("library pick requested", { multiple, imageSelectionLimit });
1412
1601
  return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
1413
1602
  ...imageOptions,
1414
1603
  // Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
@@ -1417,6 +1606,7 @@ function createSagepilotFilePicker(options) {
1417
1606
  }
1418
1607
  async function pickDocuments(multiple) {
1419
1608
  if (!documentsPicker) return [];
1609
+ debugFilePicker("document pick requested", { multiple, documentMaxFileSizeBytes });
1420
1610
  let picked;
1421
1611
  try {
1422
1612
  picked = await documentsPicker.pick({ allowMultiSelection: multiple });
@@ -1466,6 +1656,7 @@ function createSagepilotFilePicker(options) {
1466
1656
  return {
1467
1657
  sources,
1468
1658
  async pickFiles(request) {
1659
+ debugFilePicker("pickFiles request", request);
1469
1660
  if (request.source === "camera") return pickFromCamera();
1470
1661
  if (request.source === "library") return pickFromLibrary(request.multiple);
1471
1662
  return pickDocuments(request.multiple);
@@ -1475,13 +1666,17 @@ function createSagepilotFilePicker(options) {
1475
1666
 
1476
1667
  // src/ui/SagepilotChatProvider.ts
1477
1668
  var import_react = require("react");
1478
- var import_react_native2 = require("react-native");
1669
+ var import_lucide_react_native = require("lucide-react-native");
1670
+ var import_react_native3 = require("react-native");
1479
1671
  var import_react_native_webview = require("react-native-webview");
1480
1672
 
1481
1673
  // src/core/webview/mobileBridge.ts
1482
1674
  var FILE_PICKER_PROTOCOL_VERSION = 2;
1675
+ function buildBridgeCapabilitiesPayload(capabilities) {
1676
+ return { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1677
+ }
1483
1678
  function buildBridgeCapabilitiesScript(capabilities) {
1484
- const payload = { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1679
+ const payload = buildBridgeCapabilitiesPayload(capabilities);
1485
1680
  return [
1486
1681
  "(function(){",
1487
1682
  "try {",
@@ -1530,9 +1725,23 @@ function parseHostedBridgeMessage(rawData) {
1530
1725
  return null;
1531
1726
  }
1532
1727
  }
1533
- var mobileWebViewBridgeScript = `
1728
+ function buildMobileWebViewBridgeScript(capabilities) {
1729
+ const payload = buildBridgeCapabilitiesPayload(capabilities);
1730
+ return `
1534
1731
  (function () {
1535
- if (window.__sagepilotRnBridgeInstalled) return true;
1732
+ var bridgeCapabilities = ${JSON.stringify(payload)};
1733
+ var applyBridgeCapabilities = function () {
1734
+ try {
1735
+ if (window.SagepilotMobileBridge) {
1736
+ window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, bridgeCapabilities);
1737
+ }
1738
+ } catch (error) {}
1739
+ };
1740
+
1741
+ if (window.__sagepilotRnBridgeInstalled) {
1742
+ applyBridgeCapabilities();
1743
+ return true;
1744
+ }
1536
1745
  window.__sagepilotRnBridgeInstalled = true;
1537
1746
 
1538
1747
  var ensureViewport = function () {
@@ -1573,9 +1782,11 @@ var mobileWebViewBridgeScript = `
1573
1782
  };
1574
1783
 
1575
1784
  window.SagepilotMobileBridge = {
1785
+ capabilities: bridgeCapabilities,
1576
1786
  postMessage: sendToReactNative,
1577
1787
  ready: function () { sendToReactNative({ type: "sagepilot:ready" }); }
1578
1788
  };
1789
+ applyBridgeCapabilities();
1579
1790
 
1580
1791
  document.addEventListener("click", function (event) {
1581
1792
  try {
@@ -1616,6 +1827,7 @@ var mobileWebViewBridgeScript = `
1616
1827
  return true;
1617
1828
  })();
1618
1829
  `;
1830
+ }
1619
1831
 
1620
1832
  // src/specs/SagepilotInsetsViewNativeComponent.ts
1621
1833
  var import_codegenNativeComponent = __toESM(require("react-native/Libraries/Utilities/codegenNativeComponent"));
@@ -1632,13 +1844,17 @@ function readPresentationState() {
1632
1844
  presentation: internalSagepilotChat.getConfig()?.presentation
1633
1845
  };
1634
1846
  }
1847
+ function getBridgeCapabilities() {
1848
+ return {
1849
+ nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1850
+ };
1851
+ }
1635
1852
  function getInjectedWebViewScript() {
1853
+ const capabilities = getBridgeCapabilities();
1636
1854
  return [
1637
1855
  internalSagepilotChat.getHostedAuthScript(),
1638
- mobileWebViewBridgeScript,
1639
- buildBridgeCapabilitiesScript({
1640
- nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1641
- })
1856
+ buildMobileWebViewBridgeScript(capabilities),
1857
+ buildBridgeCapabilitiesScript(capabilities)
1642
1858
  ].filter(Boolean).join("\n");
1643
1859
  }
1644
1860
  var FILE_PICKER_SOURCE_LABELS = {
@@ -1646,6 +1862,22 @@ var FILE_PICKER_SOURCE_LABELS = {
1646
1862
  library: "Choose from gallery",
1647
1863
  documents: "Browse files"
1648
1864
  };
1865
+ var FILE_PICKER_SOURCE_DESCRIPTIONS = {
1866
+ camera: "Use the in-app camera",
1867
+ library: "Photos and videos on this device",
1868
+ documents: "PDFs, documents, and Drive files"
1869
+ };
1870
+ var FILE_PICKER_SOURCE_ICONS = {
1871
+ camera: import_lucide_react_native.Camera,
1872
+ library: import_lucide_react_native.Images,
1873
+ documents: import_lucide_react_native.FileText
1874
+ };
1875
+ var FILE_PICKER_SOURCE_ICON_COLORS = {
1876
+ camera: "#0284c7",
1877
+ library: "#16a34a",
1878
+ documents: "#7c3aed"
1879
+ };
1880
+ var DEBUG_PREFIX2 = "[SagepilotSDK][Provider]";
1649
1881
  var DELIVERY_RETRY_MS = 1500;
1650
1882
  var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
1651
1883
  var DELIVERY_MAX_ATTEMPTS = 8;
@@ -1655,6 +1887,7 @@ var PENDING_FILES_CACHE_KEY = "pending_batch";
1655
1887
  var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
1656
1888
  var LIVENESS_PONG_TIMEOUT_MS = 1500;
1657
1889
  var LIVENESS_MAX_PING_ATTEMPTS = 2;
1890
+ var LIVENESS_PICKER_GRACE_MS = 8e3;
1658
1891
  var LIVENESS_MAX_REMOUNTS = 2;
1659
1892
  var batchSequence = 0;
1660
1893
  function nextBatchId() {
@@ -1668,6 +1901,29 @@ function totalBase64Bytes(files) {
1668
1901
  function storageKeyFor(batchId, index) {
1669
1902
  return `${batchId}_${index}.bin`;
1670
1903
  }
1904
+ function renderFilePickerSourceIcon(source) {
1905
+ const Icon = FILE_PICKER_SOURCE_ICONS[source];
1906
+ return (0, import_react.createElement)(
1907
+ import_react_native3.View,
1908
+ { style: getFilePickerSourceIconStyle(source) },
1909
+ (0, import_react.createElement)(Icon, {
1910
+ color: FILE_PICKER_SOURCE_ICON_COLORS[source],
1911
+ size: 23,
1912
+ strokeWidth: 2.35
1913
+ })
1914
+ );
1915
+ }
1916
+ function getFilePickerSourceIconStyle(source) {
1917
+ if (source === "camera") return [styles.sheetOptionIcon, styles.cameraOptionIcon];
1918
+ if (source === "library") return [styles.sheetOptionIcon, styles.libraryOptionIcon];
1919
+ return [styles.sheetOptionIcon, styles.documentsOptionIcon];
1920
+ }
1921
+ function getSheetOptionStyle(pressed) {
1922
+ return [styles.sheetOption, pressed && styles.sheetOptionPressed];
1923
+ }
1924
+ function getSheetCancelStyle(pressed) {
1925
+ return [styles.sheetCancelButton, pressed && styles.sheetCancelButtonPressed];
1926
+ }
1671
1927
  function buildDispatchScript(messageLiteral) {
1672
1928
  return [
1673
1929
  "(function(){",
@@ -1679,8 +1935,14 @@ function buildDispatchScript(messageLiteral) {
1679
1935
  "})();"
1680
1936
  ].join("\n");
1681
1937
  }
1682
- function buildFilesPickedScript(files, batchId) {
1683
- return buildDispatchScript(`{ type: "sagepilot:files_picked", batch_id: ${JSON.stringify(batchId)}, files: ${JSON.stringify(files)} }`);
1938
+ function buildFilesPickedScript(batch) {
1939
+ return buildDispatchScript(JSON.stringify({
1940
+ type: "sagepilot:files_picked",
1941
+ batch_id: batch.batchId,
1942
+ chat_id: batch.target?.chatId,
1943
+ conversation_id: batch.target?.chatId,
1944
+ files: batch.files
1945
+ }));
1684
1946
  }
1685
1947
  function buildFilePickerErrorScript(message, code) {
1686
1948
  return buildDispatchScript(
@@ -1699,6 +1961,16 @@ function readFilePickerError(error) {
1699
1961
  }
1700
1962
  return { message: "Could not attach the selected file." };
1701
1963
  }
1964
+ function debugProvider(message, details) {
1965
+ console.log(`${DEBUG_PREFIX2} ${message}`, details ?? "");
1966
+ }
1967
+ function describeAttachmentTarget(target) {
1968
+ return {
1969
+ targetChatId: target?.chatId,
1970
+ targetRouteChatId: target?.routeChatId,
1971
+ targetScreen: target?.hostedChatView.screen
1972
+ };
1973
+ }
1702
1974
  function getHostedIdentityDispatchScript() {
1703
1975
  const message = internalSagepilotChat.getHostedIdentityMessage();
1704
1976
  if (!message) return "";
@@ -1727,7 +1999,7 @@ function isInternalWebViewScheme(url) {
1727
1999
  var hostedChatWebViewProps = {
1728
2000
  allowFileAccess: true,
1729
2001
  allowFileAccessFromFileURLs: true,
1730
- ...import_react_native2.Platform.OS === "ios" ? {
2002
+ ...import_react_native3.Platform.OS === "ios" ? {
1731
2003
  automaticallyAdjustContentInsets: false,
1732
2004
  contentInsetAdjustmentBehavior: "never",
1733
2005
  hideKeyboardAccessoryView: true
@@ -1742,7 +2014,7 @@ var hostedChatWebViewProps = {
1742
2014
  sharedCookiesEnabled: true,
1743
2015
  thirdPartyCookiesEnabled: true
1744
2016
  };
1745
- var AndroidInsetsView = import_react_native2.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native2.View;
2017
+ var AndroidInsetsView = import_react_native3.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native3.View;
1746
2018
  var emptyAndroidInsets = { top: 0, bottom: 0 };
1747
2019
  function SagepilotChatProvider({
1748
2020
  children,
@@ -1761,8 +2033,11 @@ function SagepilotChatProvider({
1761
2033
  const deliveryAttemptsRef = (0, import_react.useRef)(0);
1762
2034
  const pendingPingRef = (0, import_react.useRef)(null);
1763
2035
  const pingTimeoutRef = (0, import_react.useRef)(null);
2036
+ const livenessResumeTimerRef = (0, import_react.useRef)(null);
2037
+ const livenessPausedUntilRef = (0, import_react.useRef)(0);
2038
+ const pickerInFlightRef = (0, import_react.useRef)(false);
1764
2039
  const livenessRemountCountRef = (0, import_react.useRef)(0);
1765
- const appStateRef = (0, import_react.useRef)(import_react_native2.AppState.currentState);
2040
+ const appStateRef = (0, import_react.useRef)(import_react_native3.AppState.currentState);
1766
2041
  const didReconcileRef = (0, import_react.useRef)(false);
1767
2042
  const [androidRepaintTick, setAndroidRepaintTick] = (0, import_react.useState)(0);
1768
2043
  const [sourceChooser, setSourceChooser] = (0, import_react.useState)(null);
@@ -1783,6 +2058,7 @@ function SagepilotChatProvider({
1783
2058
  if (hasFileStore) {
1784
2059
  const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
1785
2060
  batchId: batch.batchId,
2061
+ target: batch.target,
1786
2062
  files: batch.files.map((file, index) => ({
1787
2063
  file_name: file.file_name,
1788
2064
  mime_type: file.mime_type,
@@ -1801,6 +2077,7 @@ function SagepilotChatProvider({
1801
2077
  if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
1802
2078
  const manifest = queue.map((batch) => ({
1803
2079
  batchId: batch.batchId,
2080
+ target: batch.target,
1804
2081
  files: batch.files.map((file) => ({
1805
2082
  file_name: file.file_name,
1806
2083
  mime_type: file.mime_type,
@@ -1842,12 +2119,28 @@ function SagepilotChatProvider({
1842
2119
  }
1843
2120
  const batch = pendingBatchesRef.current[0];
1844
2121
  if (!batch) return;
2122
+ if (internalSagepilotChat.restoreHostedAttachmentTarget(batch.target)) {
2123
+ debugProvider("restored hosted attachment target", {
2124
+ batchId: batch.batchId,
2125
+ ...describeAttachmentTarget(batch.target)
2126
+ });
2127
+ widgetReadyRef.current = false;
2128
+ deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
2129
+ return;
2130
+ }
1845
2131
  deliveryAttemptsRef.current += 1;
1846
2132
  const attempts = deliveryAttemptsRef.current;
1847
2133
  const ref = nativeWebViewRef.current;
1848
2134
  const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
1849
2135
  if (shouldDeliver && ref) {
1850
- ref.injectJavaScript(buildFilesPickedScript(batch.files, batch.batchId));
2136
+ debugProvider("delivering native picked files", {
2137
+ batchId: batch.batchId,
2138
+ attempt: attempts,
2139
+ widgetReady: widgetReadyRef.current,
2140
+ count: batch.files.length,
2141
+ ...describeAttachmentTarget(batch.target)
2142
+ });
2143
+ ref.injectJavaScript(buildFilesPickedScript(batch));
1851
2144
  }
1852
2145
  if (attempts < DELIVERY_MAX_ATTEMPTS) {
1853
2146
  deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
@@ -1878,23 +2171,65 @@ function SagepilotChatProvider({
1878
2171
  const batchId = nextBatchId();
1879
2172
  const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
1880
2173
  const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
1881
- const batch = { batchId, files, storageKeys };
2174
+ const batch = {
2175
+ batchId,
2176
+ files,
2177
+ storageKeys,
2178
+ target: internalSagepilotChat.getHostedAttachmentTarget()
2179
+ };
2180
+ debugProvider("queued native picked files", {
2181
+ batchId,
2182
+ count: files.length,
2183
+ totalBase64Bytes: totalBase64Bytes(files),
2184
+ persistedToFileStore: hasFileStore,
2185
+ ...describeAttachmentTarget(batch.target)
2186
+ });
1882
2187
  pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
1883
2188
  ensureDelivery();
1884
2189
  writeManifest();
1885
2190
  void writeBatchBytes(batch);
1886
2191
  }, [ensureDelivery, writeManifest, writeBatchBytes]);
1887
- const recoverNativeWebView = (0, import_react.useCallback)(() => {
2192
+ const recoverNativeWebView = (0, import_react.useCallback)((reasonOrEvent) => {
2193
+ const reason = typeof reasonOrEvent === "string" ? reasonOrEvent : "webview_render_process";
2194
+ debugProvider("recovering native WebView", { reason });
1888
2195
  widgetReadyRef.current = false;
1889
2196
  setNativeWebViewKey((key) => key + 1);
1890
2197
  }, []);
1891
2198
  const recoverPreloadWebView = (0, import_react.useCallback)(() => {
1892
2199
  setPreloadWebViewKey((key) => key + 1);
1893
2200
  }, []);
2201
+ const pauseLivenessAfterPicker = (0, import_react.useCallback)((durationMs) => {
2202
+ if (import_react_native3.Platform.OS !== "android") return;
2203
+ livenessPausedUntilRef.current = Math.max(livenessPausedUntilRef.current, Date.now() + durationMs);
2204
+ }, []);
1894
2205
  const runNativeFilePicker = (0, import_react.useCallback)((source, multiple) => {
1895
2206
  const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1896
- if (!filePicker) return;
2207
+ if (!filePicker) {
2208
+ debugProvider("native file picker requested but no adapter is configured", { source, multiple });
2209
+ return;
2210
+ }
2211
+ if (pickerInFlightRef.current) {
2212
+ debugProvider("native file picker ignored because another picker is in flight", { source, multiple });
2213
+ return;
2214
+ }
2215
+ debugProvider("native file picker starting", {
2216
+ source,
2217
+ multiple,
2218
+ configuredSources: filePicker.sources
2219
+ });
2220
+ pauseLivenessAfterPicker(6e4);
2221
+ pickerInFlightRef.current = true;
1897
2222
  filePicker.pickFiles({ source, multiple }).then((files) => {
2223
+ debugProvider("native file picker resolved", {
2224
+ source,
2225
+ count: files.length,
2226
+ files: files.map((file) => ({
2227
+ fileName: file.file_name,
2228
+ mimeType: file.mime_type,
2229
+ size: file.size,
2230
+ base64Length: file.data_base64.length
2231
+ }))
2232
+ });
1898
2233
  if (files.length === 0) {
1899
2234
  nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
1900
2235
  return;
@@ -1902,14 +2237,26 @@ function SagepilotChatProvider({
1902
2237
  queuePickedFiles(files);
1903
2238
  }).catch((error) => {
1904
2239
  const { message, code } = readFilePickerError(error);
2240
+ debugProvider("native file picker failed", { source, code, message });
1905
2241
  nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
2242
+ }).finally(() => {
2243
+ pickerInFlightRef.current = false;
2244
+ pauseLivenessAfterPicker(LIVENESS_PICKER_GRACE_MS);
1906
2245
  });
1907
- }, [queuePickedFiles]);
2246
+ }, [pauseLivenessAfterPicker, queuePickedFiles]);
1908
2247
  const openNativeFilePicker = (0, import_react.useCallback)((multiple) => {
1909
2248
  const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1910
- if (!filePicker || filePicker.sources.length === 0) return;
2249
+ if (!filePicker || filePicker.sources.length === 0) {
2250
+ debugProvider("open native file picker ignored", {
2251
+ multiple,
2252
+ hasAdapter: Boolean(filePicker),
2253
+ sources: filePicker?.sources ?? []
2254
+ });
2255
+ return;
2256
+ }
2257
+ debugProvider("open native file picker", { multiple, sources: filePicker.sources });
1911
2258
  const [onlySource] = filePicker.sources;
1912
- if (filePicker.sources.length === 1 && onlySource) {
2259
+ if (filePicker.sources.length === 1 && onlySource && onlySource !== "camera") {
1913
2260
  runNativeFilePicker(onlySource, multiple);
1914
2261
  return;
1915
2262
  }
@@ -1962,7 +2309,8 @@ function SagepilotChatProvider({
1962
2309
  restored.push({
1963
2310
  batchId: batch.batchId,
1964
2311
  files,
1965
- storageKeys: storageKeys.length === files.length ? storageKeys : void 0
2312
+ storageKeys: storageKeys.length === files.length ? storageKeys : void 0,
2313
+ target: batch.target
1966
2314
  });
1967
2315
  }
1968
2316
  }
@@ -1983,6 +2331,19 @@ function SagepilotChatProvider({
1983
2331
  cancelled = true;
1984
2332
  };
1985
2333
  }, [getPendingCache, startDelivery, writeManifest]);
2334
+ const clearPing = (0, import_react.useCallback)(() => {
2335
+ pendingPingRef.current = null;
2336
+ if (pingTimeoutRef.current) {
2337
+ clearTimeout(pingTimeoutRef.current);
2338
+ pingTimeoutRef.current = null;
2339
+ }
2340
+ }, []);
2341
+ const clearDeferredLivenessProbe = (0, import_react.useCallback)(() => {
2342
+ if (livenessResumeTimerRef.current) {
2343
+ clearTimeout(livenessResumeTimerRef.current);
2344
+ livenessResumeTimerRef.current = null;
2345
+ }
2346
+ }, []);
1986
2347
  (0, import_react.useEffect)(() => {
1987
2348
  return () => {
1988
2349
  if (deliveryTimerRef.current) {
@@ -1993,19 +2354,27 @@ function SagepilotChatProvider({
1993
2354
  clearTimeout(pingTimeoutRef.current);
1994
2355
  pingTimeoutRef.current = null;
1995
2356
  }
2357
+ clearDeferredLivenessProbe();
1996
2358
  };
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
- }, []);
2359
+ }, [clearDeferredLivenessProbe]);
2005
2360
  const runLivenessProbe = (0, import_react.useCallback)(() => {
2006
- if (import_react_native2.Platform.OS !== "android") return;
2361
+ if (import_react_native3.Platform.OS !== "android") return;
2007
2362
  const ref = nativeWebViewRef.current;
2008
2363
  if (!ref || !internalSagepilotChat.isPresented()) return;
2364
+ const pauseRemainingMs = livenessPausedUntilRef.current - Date.now();
2365
+ if (pickerInFlightRef.current || pauseRemainingMs > 0) {
2366
+ const retryDelayMs = pickerInFlightRef.current ? 500 : Math.max(0, pauseRemainingMs);
2367
+ debugProvider("liveness probe deferred after picker", {
2368
+ pickerInFlight: pickerInFlightRef.current,
2369
+ retryDelayMs
2370
+ });
2371
+ clearDeferredLivenessProbe();
2372
+ livenessResumeTimerRef.current = setTimeout(() => {
2373
+ livenessResumeTimerRef.current = null;
2374
+ runLivenessProbe();
2375
+ }, retryDelayMs);
2376
+ return;
2377
+ }
2009
2378
  setAndroidRepaintTick((tick) => tick + 1);
2010
2379
  const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
2011
2380
  const nonce = `${Date.now().toString(36)}_${attempts}`;
@@ -2015,14 +2384,14 @@ function SagepilotChatProvider({
2015
2384
  pingTimeoutRef.current = setTimeout(() => {
2016
2385
  if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
2017
2386
  clearPing();
2018
- recoverNativeWebView();
2387
+ recoverNativeWebView("liveness_timeout");
2019
2388
  return;
2020
2389
  }
2021
2390
  runLivenessProbe();
2022
2391
  }, LIVENESS_PONG_TIMEOUT_MS);
2023
- }, [clearPing, recoverNativeWebView]);
2392
+ }, [clearDeferredLivenessProbe, clearPing, recoverNativeWebView]);
2024
2393
  (0, import_react.useEffect)(() => {
2025
- const subscription = import_react_native2.AppState.addEventListener("change", (nextState) => {
2394
+ const subscription = import_react_native3.AppState.addEventListener("change", (nextState) => {
2026
2395
  const prev = appStateRef.current;
2027
2396
  appStateRef.current = nextState;
2028
2397
  if (nextState === "active" && (prev === "background" || prev === "inactive")) {
@@ -2050,11 +2419,11 @@ function SagepilotChatProvider({
2050
2419
  const presentationStyle = state.presentation?.style ?? "sheet";
2051
2420
  const isFullScreenModal = presentationStyle === "fullScreen";
2052
2421
  const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
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" ? {
2422
+ const ModalContainer = import_react_native3.Platform.OS === "ios" && isFullScreenModal ? import_react_native3.SafeAreaView : import_react_native3.View;
2423
+ const NativeModalContainer = import_react_native3.Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
2424
+ const ChatContentContainer = import_react_native3.Platform.OS === "ios" ? import_react_native3.KeyboardAvoidingView : import_react_native3.View;
2425
+ const nativeModalContainerProps = import_react_native3.Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
2426
+ const chatContentContainerProps = import_react_native3.Platform.OS === "ios" ? {
2058
2427
  behavior: "padding",
2059
2428
  enabled: true,
2060
2429
  keyboardVerticalOffset: 0,
@@ -2062,7 +2431,7 @@ function SagepilotChatProvider({
2062
2431
  } : {
2063
2432
  style: [
2064
2433
  styles.modalContent,
2065
- import_react_native2.Platform.OS === "android" ? {
2434
+ import_react_native3.Platform.OS === "android" ? {
2066
2435
  paddingTop: androidModalInsets.top,
2067
2436
  paddingBottom: androidModalInsets.bottom
2068
2437
  } : null
@@ -2083,6 +2452,11 @@ function SagepilotChatProvider({
2083
2452
  const ackBatchId = message.batch_id;
2084
2453
  const head = pendingBatchesRef.current[0];
2085
2454
  if (head && (!ackBatchId || ackBatchId === head.batchId)) {
2455
+ debugProvider("native picked files acknowledged", {
2456
+ batchId: head.batchId,
2457
+ ackBatchId,
2458
+ count: message.count
2459
+ });
2086
2460
  acknowledgeHeadBatch();
2087
2461
  }
2088
2462
  return;
@@ -2092,9 +2466,13 @@ function SagepilotChatProvider({
2092
2466
  if (pending && (!message.nonce || message.nonce === pending.nonce)) {
2093
2467
  if (message.alive === false) {
2094
2468
  clearPing();
2469
+ debugProvider("liveness probe reported dead widget", {
2470
+ supportsHeartbeat: message.supportsHeartbeat,
2471
+ remountCount: livenessRemountCountRef.current
2472
+ });
2095
2473
  if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
2096
2474
  livenessRemountCountRef.current += 1;
2097
- recoverNativeWebView();
2475
+ recoverNativeWebView("liveness_dead_pong");
2098
2476
  }
2099
2477
  } else {
2100
2478
  livenessRemountCountRef.current = 0;
@@ -2116,7 +2494,13 @@ function SagepilotChatProvider({
2116
2494
  if (!script || !nativeWebViewRef.current) return;
2117
2495
  nativeWebViewRef.current.injectJavaScript(script);
2118
2496
  };
2497
+ const injectNativeBridgeToNativeWebView = () => {
2498
+ if (!nativeWebViewRef.current) return;
2499
+ nativeWebViewRef.current.injectJavaScript(getInjectedWebViewScript());
2500
+ };
2119
2501
  const handleNativeWebViewLoadEnd = () => {
2502
+ debugProvider("native WebView load end");
2503
+ injectNativeBridgeToNativeWebView();
2120
2504
  postIdentityToNativeWebView();
2121
2505
  widgetReadyRef.current = false;
2122
2506
  startDelivery();
@@ -2130,11 +2514,11 @@ function SagepilotChatProvider({
2130
2514
  if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
2131
2515
  return true;
2132
2516
  }
2133
- import_react_native2.Linking.openURL(url).catch(() => void 0);
2517
+ import_react_native3.Linking.openURL(url).catch(() => void 0);
2134
2518
  return false;
2135
2519
  }, [state.conversationUrl, state.preloadUrl]);
2136
2520
  (0, import_react.useEffect)(() => {
2137
- if (import_react_native2.Platform.OS !== "web" || typeof window === "undefined") return;
2521
+ if (import_react_native3.Platform.OS !== "web" || typeof window === "undefined") return;
2138
2522
  const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
2139
2523
  if (!trustedWidgetOrigin) return;
2140
2524
  const handleWindowMessage = (event) => {
@@ -2145,16 +2529,16 @@ function SagepilotChatProvider({
2145
2529
  return () => window.removeEventListener("message", handleWindowMessage);
2146
2530
  }, [state.conversationUrl]);
2147
2531
  (0, import_react.useEffect)(() => {
2148
- if (import_react_native2.Platform.OS !== "web" || !state.isPresented) return;
2532
+ if (import_react_native3.Platform.OS !== "web" || !state.isPresented) return;
2149
2533
  postIdentityToWebFrame();
2150
2534
  }, [state.isPresented, state.conversationUrl, state.configured]);
2151
2535
  (0, import_react.useEffect)(() => {
2152
- if (import_react_native2.Platform.OS === "web" || !state.isPresented) return;
2536
+ if (import_react_native3.Platform.OS === "web" || !state.isPresented) return;
2153
2537
  postIdentityToNativeWebView();
2154
2538
  }, [state.isPresented, state.conversationUrl, state.configured]);
2155
- if (import_react_native2.Platform.OS === "web") {
2539
+ if (import_react_native3.Platform.OS === "web") {
2156
2540
  return (0, import_react.createElement)(
2157
- import_react_native2.View,
2541
+ import_react_native3.View,
2158
2542
  { style: styles.root },
2159
2543
  children,
2160
2544
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)("iframe", {
@@ -2163,20 +2547,20 @@ function SagepilotChatProvider({
2163
2547
  title: "Sagepilot chat preload"
2164
2548
  }) : null,
2165
2549
  state.configured && state.isPresented && state.conversationUrl ? (0, import_react.createElement)(
2166
- import_react_native2.View,
2550
+ import_react_native3.View,
2167
2551
  { style: styles.webOverlay },
2168
2552
  (0, import_react.createElement)(
2169
- import_react_native2.View,
2553
+ import_react_native3.View,
2170
2554
  { style: styles.webPanel },
2171
2555
  showCloseButton ? (0, import_react.createElement)(
2172
- import_react_native2.Pressable,
2556
+ import_react_native3.Pressable,
2173
2557
  {
2174
2558
  accessibilityRole: "button",
2175
2559
  accessibilityLabel: closeLabel,
2176
2560
  onPress: () => internalSagepilotChat.dismiss(),
2177
2561
  style: styles.webCloseButton
2178
2562
  },
2179
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.closeText }, closeLabel)
2563
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.closeText }, closeLabel)
2180
2564
  ) : null,
2181
2565
  (0, import_react.createElement)("iframe", {
2182
2566
  ref: webFrameRef,
@@ -2190,7 +2574,7 @@ function SagepilotChatProvider({
2190
2574
  );
2191
2575
  }
2192
2576
  return (0, import_react.createElement)(
2193
- import_react_native2.View,
2577
+ import_react_native3.View,
2194
2578
  { style: styles.root },
2195
2579
  children,
2196
2580
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
@@ -2209,13 +2593,13 @@ function SagepilotChatProvider({
2209
2593
  onContentProcessDidTerminate: recoverPreloadWebView
2210
2594
  }) : null,
2211
2595
  (0, import_react.createElement)(
2212
- import_react_native2.Modal,
2596
+ import_react_native3.Modal,
2213
2597
  {
2214
2598
  visible: state.configured && state.isPresented,
2215
2599
  animationType,
2216
2600
  presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
2217
- statusBarTranslucent: import_react_native2.Platform.OS === "android",
2218
- navigationBarTranslucent: import_react_native2.Platform.OS === "android",
2601
+ statusBarTranslucent: import_react_native3.Platform.OS === "android",
2602
+ navigationBarTranslucent: import_react_native3.Platform.OS === "android",
2219
2603
  onRequestClose: () => internalSagepilotChat.dismiss()
2220
2604
  },
2221
2605
  (0, import_react.createElement)(
@@ -2225,17 +2609,17 @@ function SagepilotChatProvider({
2225
2609
  ChatContentContainer,
2226
2610
  chatContentContainerProps,
2227
2611
  showCloseButton ? (0, import_react.createElement)(
2228
- import_react_native2.View,
2612
+ import_react_native3.View,
2229
2613
  { style: styles.header },
2230
2614
  (0, import_react.createElement)(
2231
- import_react_native2.Pressable,
2615
+ import_react_native3.Pressable,
2232
2616
  {
2233
2617
  accessibilityRole: "button",
2234
2618
  accessibilityLabel: closeLabel,
2235
2619
  onPress: () => internalSagepilotChat.dismiss(),
2236
2620
  style: styles.closeButton
2237
2621
  },
2238
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.closeText }, closeLabel)
2622
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.closeText }, closeLabel)
2239
2623
  )
2240
2624
  ) : null,
2241
2625
  state.conversationUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
@@ -2246,7 +2630,7 @@ function SagepilotChatProvider({
2246
2630
  // The imperceptible opacity toggle forces the Android WebView
2247
2631
  // surface to re-composite on resume, clearing the blank-but-alive
2248
2632
  // 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,
2633
+ style: import_react_native3.Platform.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
2250
2634
  startInLoadingState: true,
2251
2635
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
2252
2636
  onMessage: handleWebViewMessage,
@@ -2258,10 +2642,10 @@ function SagepilotChatProvider({
2258
2642
  // iOS equivalent: WKWebView content process terminated.
2259
2643
  onContentProcessDidTerminate: recoverNativeWebView,
2260
2644
  renderLoading: () => (0, import_react.createElement)(
2261
- import_react_native2.View,
2645
+ import_react_native3.View,
2262
2646
  { style: styles.loading },
2263
- (0, import_react.createElement)(import_react_native2.ActivityIndicator, null),
2264
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.loadingText }, loadingLabel)
2647
+ (0, import_react.createElement)(import_react_native3.ActivityIndicator, null),
2648
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.loadingText }, loadingLabel)
2265
2649
  )
2266
2650
  }) : null
2267
2651
  )
@@ -2270,7 +2654,7 @@ function SagepilotChatProvider({
2270
2654
  // Native attachment-source chooser. Replaces the Android Alert (capped at
2271
2655
  // 3 buttons) so camera/library/documents all show on every platform.
2272
2656
  (0, import_react.createElement)(
2273
- import_react_native2.Modal,
2657
+ import_react_native3.Modal,
2274
2658
  {
2275
2659
  visible: sourceChooser !== null,
2276
2660
  transparent: true,
@@ -2279,39 +2663,50 @@ function SagepilotChatProvider({
2279
2663
  onRequestClose: () => setSourceChooser(null)
2280
2664
  },
2281
2665
  (0, import_react.createElement)(
2282
- import_react_native2.Pressable,
2666
+ import_react_native3.Pressable,
2283
2667
  { style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
2284
2668
  (0, import_react.createElement)(
2285
- import_react_native2.View,
2669
+ import_react_native3.View,
2286
2670
  { style: styles.sheetCard },
2287
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetTitle }, "Add attachment"),
2671
+ (0, import_react.createElement)(import_react_native3.View, { style: styles.sheetHandle }),
2672
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetTitle }, "Add attachment"),
2288
2673
  ...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
2289
2674
  (source) => (0, import_react.createElement)(
2290
- import_react_native2.Pressable,
2675
+ import_react_native3.Pressable,
2291
2676
  {
2292
2677
  key: source,
2293
2678
  accessibilityRole: "button",
2294
- style: styles.sheetButton,
2679
+ android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
2680
+ hitSlop: 4,
2681
+ style: ({ pressed }) => getSheetOptionStyle(pressed),
2295
2682
  onPress: () => handleSourceChoice(source)
2296
2683
  },
2297
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source])
2684
+ renderFilePickerSourceIcon(source),
2685
+ (0, import_react.createElement)(
2686
+ import_react_native3.View,
2687
+ { style: styles.sheetOptionText },
2688
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source]),
2689
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetButtonDescription }, FILE_PICKER_SOURCE_DESCRIPTIONS[source])
2690
+ )
2298
2691
  )
2299
2692
  ),
2300
2693
  (0, import_react.createElement)(
2301
- import_react_native2.Pressable,
2694
+ import_react_native3.Pressable,
2302
2695
  {
2303
2696
  accessibilityRole: "button",
2304
- style: [styles.sheetButton, styles.sheetCancelButton],
2697
+ android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
2698
+ style: ({ pressed }) => getSheetCancelStyle(pressed),
2305
2699
  onPress: () => setSourceChooser(null)
2306
2700
  },
2307
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetCancelText }, "Cancel")
2701
+ (0, import_react.createElement)(import_lucide_react_native.X, { color: "#334155", size: 18, strokeWidth: 2.5, style: styles.sheetCancelIcon }),
2702
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetCancelText }, "Cancel")
2308
2703
  )
2309
2704
  )
2310
2705
  )
2311
2706
  )
2312
2707
  );
2313
2708
  }
2314
- var styles = import_react_native2.StyleSheet.create({
2709
+ var styles = import_react_native3.StyleSheet.create({
2315
2710
  root: {
2316
2711
  flex: 1
2317
2712
  },
@@ -2397,7 +2792,7 @@ var styles = import_react_native2.StyleSheet.create({
2397
2792
  borderStyle: "none"
2398
2793
  },
2399
2794
  loading: {
2400
- ...import_react_native2.StyleSheet.absoluteFillObject,
2795
+ ...import_react_native3.StyleSheet.absoluteFillObject,
2401
2796
  alignItems: "center",
2402
2797
  justifyContent: "center",
2403
2798
  backgroundColor: "#ffffff"
@@ -2409,43 +2804,109 @@ var styles = import_react_native2.StyleSheet.create({
2409
2804
  },
2410
2805
  sheetBackdrop: {
2411
2806
  flex: 1,
2807
+ alignItems: "center",
2412
2808
  justifyContent: "flex-end",
2413
- backgroundColor: "rgba(17, 24, 39, 0.36)"
2809
+ backgroundColor: "rgba(15, 23, 42, 0.48)"
2414
2810
  },
2415
2811
  sheetCard: {
2812
+ width: "100%",
2813
+ maxWidth: 540,
2416
2814
  backgroundColor: "#ffffff",
2417
- borderTopLeftRadius: 16,
2418
- borderTopRightRadius: 16,
2419
- paddingTop: 8,
2420
- paddingBottom: 24,
2421
- paddingHorizontal: 8
2815
+ borderTopLeftRadius: 26,
2816
+ borderTopRightRadius: 26,
2817
+ paddingTop: 10,
2818
+ paddingBottom: 18,
2819
+ paddingHorizontal: 14,
2820
+ shadowColor: "#0f172a",
2821
+ shadowOpacity: 0.22,
2822
+ shadowRadius: 28,
2823
+ shadowOffset: { width: 0, height: -10 },
2824
+ elevation: 18
2825
+ },
2826
+ sheetHandle: {
2827
+ alignSelf: "center",
2828
+ width: 42,
2829
+ height: 5,
2830
+ borderRadius: 999,
2831
+ backgroundColor: "#d1d5db",
2832
+ marginBottom: 14
2422
2833
  },
2423
2834
  sheetTitle: {
2424
- textAlign: "center",
2425
- color: "#6b7280",
2426
- fontSize: 13,
2427
- fontWeight: "600",
2428
- paddingVertical: 10
2835
+ color: "#111827",
2836
+ fontSize: 17,
2837
+ fontWeight: "700",
2838
+ paddingBottom: 12,
2839
+ paddingHorizontal: 4
2429
2840
  },
2430
- sheetButton: {
2431
- minHeight: 52,
2841
+ sheetOption: {
2842
+ minHeight: 70,
2843
+ flexDirection: "row",
2844
+ alignItems: "center",
2845
+ borderRadius: 18,
2846
+ paddingHorizontal: 12,
2847
+ marginBottom: 6,
2848
+ backgroundColor: "#ffffff"
2849
+ },
2850
+ sheetOptionPressed: {
2851
+ backgroundColor: "#f8fafc",
2852
+ transform: [{ scale: 0.985 }]
2853
+ },
2854
+ sheetOptionIcon: {
2855
+ width: 46,
2856
+ height: 46,
2857
+ borderRadius: 16,
2858
+ marginRight: 14,
2432
2859
  alignItems: "center",
2433
2860
  justifyContent: "center",
2434
- borderRadius: 12
2861
+ position: "relative",
2862
+ overflow: "hidden"
2863
+ },
2864
+ sheetOptionText: {
2865
+ flex: 1,
2866
+ justifyContent: "center"
2435
2867
  },
2436
2868
  sheetButtonText: {
2437
- color: "#111827",
2869
+ color: "#0f172a",
2438
2870
  fontSize: 16,
2439
- fontWeight: "500"
2871
+ fontWeight: "700"
2872
+ },
2873
+ sheetButtonDescription: {
2874
+ color: "#64748b",
2875
+ fontSize: 13,
2876
+ fontWeight: "500",
2877
+ marginTop: 3
2878
+ },
2879
+ cameraOptionIcon: {
2880
+ backgroundColor: "#e0f2fe"
2881
+ },
2882
+ libraryOptionIcon: {
2883
+ backgroundColor: "#ecfdf5"
2884
+ },
2885
+ documentsOptionIcon: {
2886
+ backgroundColor: "#f5f3ff"
2440
2887
  },
2441
2888
  sheetCancelButton: {
2442
- marginTop: 6,
2443
- backgroundColor: "#f3f4f6"
2889
+ minHeight: 56,
2890
+ flexDirection: "row",
2891
+ alignItems: "center",
2892
+ justifyContent: "center",
2893
+ borderRadius: 18,
2894
+ marginTop: 8,
2895
+ backgroundColor: "#f1f5f9"
2896
+ },
2897
+ sheetCancelButtonPressed: {
2898
+ backgroundColor: "#e2e8f0",
2899
+ transform: [{ scale: 0.985 }]
2900
+ },
2901
+ sheetCancelIcon: {
2902
+ width: 18,
2903
+ height: 18,
2904
+ marginRight: 8
2444
2905
  },
2445
2906
  sheetCancelText: {
2446
- color: "#111827",
2907
+ color: "#0f172a",
2447
2908
  fontSize: 16,
2448
- fontWeight: "600"
2909
+ fontWeight: "700"
2449
2910
  }
2450
2911
  });
2451
2912
 
@@ -2500,6 +2961,9 @@ function useSagepilotChat() {
2500
2961
  }
2501
2962
  // Annotate the CommonJS export names for ESM import in node:
2502
2963
  0 && (module.exports = {
2964
+ SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
2965
+ SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
2966
+ SAGEPILOT_DEFAULT_IMAGE_QUALITY,
2503
2967
  SagepilotChat,
2504
2968
  SagepilotChatError,
2505
2969
  SagepilotChatProvider,
@@ -2508,5 +2972,7 @@ function useSagepilotChat() {
2508
2972
  createKeychainTokenStorage,
2509
2973
  createSagepilotFilePicker,
2510
2974
  createSagepilotFileStore,
2975
+ ensureSagepilotAndroidCameraPermission,
2976
+ promptOpenSagepilotCameraSettings,
2511
2977
  useSagepilotChat
2512
2978
  });