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

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.1";
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,29 @@ 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 DEFAULT_IMAGE_MAX_DIMENSION = SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION;
1420
+ var DEFAULT_IMAGE_QUALITY = SAGEPILOT_DEFAULT_IMAGE_QUALITY;
1421
+ var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
1422
+ var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
1423
+ var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
1424
+ var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1425
+ var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1288
1426
  function bytesToMb(bytes) {
1289
1427
  return Math.round(bytes / (1024 * 1024) * 10) / 10;
1290
1428
  }
@@ -1294,27 +1432,53 @@ function estimateBase64ByteSize(dataBase64) {
1294
1432
  function stripFileScheme(uri) {
1295
1433
  return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
1296
1434
  }
1297
- async function ensureAndroidCameraPermission() {
1298
- if (import_react_native.Platform.OS !== "android") return;
1435
+ async function promptOpenSagepilotCameraSettings() {
1436
+ if (import_react_native2.Platform.OS !== "android") return;
1437
+ await new Promise((resolve) => {
1438
+ import_react_native2.Alert.alert(
1439
+ "Camera access is off",
1440
+ "Enable camera access for this app in Settings to take a photo.",
1441
+ [
1442
+ {
1443
+ text: "Not now",
1444
+ style: "cancel",
1445
+ onPress: resolve
1446
+ },
1447
+ {
1448
+ text: "Open Settings",
1449
+ onPress: () => {
1450
+ import_react_native2.Linking.openSettings().finally(resolve);
1451
+ }
1452
+ }
1453
+ ]
1454
+ );
1455
+ });
1456
+ }
1457
+ async function ensureSagepilotAndroidCameraPermission() {
1458
+ if (import_react_native2.Platform.OS !== "android") return;
1299
1459
  let result;
1300
1460
  try {
1301
- result = await import_react_native.PermissionsAndroid.request(import_react_native.PermissionsAndroid.PERMISSIONS.CAMERA);
1461
+ result = await import_react_native2.PermissionsAndroid.request(import_react_native2.PermissionsAndroid.PERMISSIONS.CAMERA);
1302
1462
  } catch {
1303
1463
  return;
1304
1464
  }
1305
- if (result === import_react_native.PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
1465
+ if (result === import_react_native2.PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
1466
+ await promptOpenSagepilotCameraSettings();
1306
1467
  throw new SagepilotFilePickerError(
1307
1468
  "permission_denied",
1308
1469
  "Camera access is turned off. Enable it for this app in Settings to take a photo."
1309
1470
  );
1310
1471
  }
1311
- if (result === import_react_native.PermissionsAndroid.RESULTS.DENIED) {
1472
+ if (result === import_react_native2.PermissionsAndroid.RESULTS.DENIED) {
1312
1473
  throw new SagepilotFilePickerError(
1313
1474
  "permission_denied",
1314
1475
  "Camera permission is required to take a photo."
1315
1476
  );
1316
1477
  }
1317
1478
  }
1479
+ function canUseCameraSource(imagePicker) {
1480
+ return Boolean(imagePicker);
1481
+ }
1318
1482
  function mapImagePickerErrorCode(errorCode) {
1319
1483
  switch (errorCode) {
1320
1484
  case "permission":
@@ -1391,7 +1555,8 @@ function createSagepilotFilePicker(options) {
1391
1555
  const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
1392
1556
  const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1393
1557
  const sources = [];
1394
- if (imagePicker) sources.push("camera", "library");
1558
+ if (canUseCameraSource(imagePicker)) sources.push("camera");
1559
+ if (imagePicker) sources.push("library");
1395
1560
  if (documentsPicker) sources.push("documents");
1396
1561
  if (sources.length === 0) return void 0;
1397
1562
  const imageOptions = {
@@ -1403,8 +1568,10 @@ function createSagepilotFilePicker(options) {
1403
1568
  saveToPhotos: false
1404
1569
  };
1405
1570
  async function pickFromCamera() {
1406
- if (!imagePicker) return [];
1407
- await ensureAndroidCameraPermission();
1571
+ await ensureSagepilotAndroidCameraPermission();
1572
+ if (!imagePicker) {
1573
+ throw new SagepilotFilePickerError("camera_unavailable", "The camera picker is not available in this build.");
1574
+ }
1408
1575
  return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
1409
1576
  }
1410
1577
  async function pickFromLibrary(multiple) {
@@ -1475,13 +1642,17 @@ function createSagepilotFilePicker(options) {
1475
1642
 
1476
1643
  // src/ui/SagepilotChatProvider.ts
1477
1644
  var import_react = require("react");
1478
- var import_react_native2 = require("react-native");
1645
+ var import_lucide_react_native = require("lucide-react-native");
1646
+ var import_react_native3 = require("react-native");
1479
1647
  var import_react_native_webview = require("react-native-webview");
1480
1648
 
1481
1649
  // src/core/webview/mobileBridge.ts
1482
1650
  var FILE_PICKER_PROTOCOL_VERSION = 2;
1651
+ function buildBridgeCapabilitiesPayload(capabilities) {
1652
+ return { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1653
+ }
1483
1654
  function buildBridgeCapabilitiesScript(capabilities) {
1484
- const payload = { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1655
+ const payload = buildBridgeCapabilitiesPayload(capabilities);
1485
1656
  return [
1486
1657
  "(function(){",
1487
1658
  "try {",
@@ -1530,9 +1701,23 @@ function parseHostedBridgeMessage(rawData) {
1530
1701
  return null;
1531
1702
  }
1532
1703
  }
1533
- var mobileWebViewBridgeScript = `
1704
+ function buildMobileWebViewBridgeScript(capabilities) {
1705
+ const payload = buildBridgeCapabilitiesPayload(capabilities);
1706
+ return `
1534
1707
  (function () {
1535
- if (window.__sagepilotRnBridgeInstalled) return true;
1708
+ var bridgeCapabilities = ${JSON.stringify(payload)};
1709
+ var applyBridgeCapabilities = function () {
1710
+ try {
1711
+ if (window.SagepilotMobileBridge) {
1712
+ window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, bridgeCapabilities);
1713
+ }
1714
+ } catch (error) {}
1715
+ };
1716
+
1717
+ if (window.__sagepilotRnBridgeInstalled) {
1718
+ applyBridgeCapabilities();
1719
+ return true;
1720
+ }
1536
1721
  window.__sagepilotRnBridgeInstalled = true;
1537
1722
 
1538
1723
  var ensureViewport = function () {
@@ -1573,9 +1758,11 @@ var mobileWebViewBridgeScript = `
1573
1758
  };
1574
1759
 
1575
1760
  window.SagepilotMobileBridge = {
1761
+ capabilities: bridgeCapabilities,
1576
1762
  postMessage: sendToReactNative,
1577
1763
  ready: function () { sendToReactNative({ type: "sagepilot:ready" }); }
1578
1764
  };
1765
+ applyBridgeCapabilities();
1579
1766
 
1580
1767
  document.addEventListener("click", function (event) {
1581
1768
  try {
@@ -1616,6 +1803,7 @@ var mobileWebViewBridgeScript = `
1616
1803
  return true;
1617
1804
  })();
1618
1805
  `;
1806
+ }
1619
1807
 
1620
1808
  // src/specs/SagepilotInsetsViewNativeComponent.ts
1621
1809
  var import_codegenNativeComponent = __toESM(require("react-native/Libraries/Utilities/codegenNativeComponent"));
@@ -1632,13 +1820,17 @@ function readPresentationState() {
1632
1820
  presentation: internalSagepilotChat.getConfig()?.presentation
1633
1821
  };
1634
1822
  }
1823
+ function getBridgeCapabilities() {
1824
+ return {
1825
+ nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1826
+ };
1827
+ }
1635
1828
  function getInjectedWebViewScript() {
1829
+ const capabilities = getBridgeCapabilities();
1636
1830
  return [
1637
1831
  internalSagepilotChat.getHostedAuthScript(),
1638
- mobileWebViewBridgeScript,
1639
- buildBridgeCapabilitiesScript({
1640
- nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1641
- })
1832
+ buildMobileWebViewBridgeScript(capabilities),
1833
+ buildBridgeCapabilitiesScript(capabilities)
1642
1834
  ].filter(Boolean).join("\n");
1643
1835
  }
1644
1836
  var FILE_PICKER_SOURCE_LABELS = {
@@ -1646,6 +1838,21 @@ var FILE_PICKER_SOURCE_LABELS = {
1646
1838
  library: "Choose from gallery",
1647
1839
  documents: "Browse files"
1648
1840
  };
1841
+ var FILE_PICKER_SOURCE_DESCRIPTIONS = {
1842
+ camera: "Use the in-app camera",
1843
+ library: "Photos and videos on this device",
1844
+ documents: "PDFs, documents, and Drive files"
1845
+ };
1846
+ var FILE_PICKER_SOURCE_ICONS = {
1847
+ camera: import_lucide_react_native.Camera,
1848
+ library: import_lucide_react_native.Images,
1849
+ documents: import_lucide_react_native.FileText
1850
+ };
1851
+ var FILE_PICKER_SOURCE_ICON_COLORS = {
1852
+ camera: "#0284c7",
1853
+ library: "#16a34a",
1854
+ documents: "#7c3aed"
1855
+ };
1649
1856
  var DELIVERY_RETRY_MS = 1500;
1650
1857
  var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
1651
1858
  var DELIVERY_MAX_ATTEMPTS = 8;
@@ -1655,6 +1862,7 @@ var PENDING_FILES_CACHE_KEY = "pending_batch";
1655
1862
  var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
1656
1863
  var LIVENESS_PONG_TIMEOUT_MS = 1500;
1657
1864
  var LIVENESS_MAX_PING_ATTEMPTS = 2;
1865
+ var LIVENESS_PICKER_GRACE_MS = 8e3;
1658
1866
  var LIVENESS_MAX_REMOUNTS = 2;
1659
1867
  var batchSequence = 0;
1660
1868
  function nextBatchId() {
@@ -1668,6 +1876,29 @@ function totalBase64Bytes(files) {
1668
1876
  function storageKeyFor(batchId, index) {
1669
1877
  return `${batchId}_${index}.bin`;
1670
1878
  }
1879
+ function renderFilePickerSourceIcon(source) {
1880
+ const Icon = FILE_PICKER_SOURCE_ICONS[source];
1881
+ return (0, import_react.createElement)(
1882
+ import_react_native3.View,
1883
+ { style: getFilePickerSourceIconStyle(source) },
1884
+ (0, import_react.createElement)(Icon, {
1885
+ color: FILE_PICKER_SOURCE_ICON_COLORS[source],
1886
+ size: 23,
1887
+ strokeWidth: 2.35
1888
+ })
1889
+ );
1890
+ }
1891
+ function getFilePickerSourceIconStyle(source) {
1892
+ if (source === "camera") return [styles.sheetOptionIcon, styles.cameraOptionIcon];
1893
+ if (source === "library") return [styles.sheetOptionIcon, styles.libraryOptionIcon];
1894
+ return [styles.sheetOptionIcon, styles.documentsOptionIcon];
1895
+ }
1896
+ function getSheetOptionStyle(pressed) {
1897
+ return [styles.sheetOption, pressed && styles.sheetOptionPressed];
1898
+ }
1899
+ function getSheetCancelStyle(pressed) {
1900
+ return [styles.sheetCancelButton, pressed && styles.sheetCancelButtonPressed];
1901
+ }
1671
1902
  function buildDispatchScript(messageLiteral) {
1672
1903
  return [
1673
1904
  "(function(){",
@@ -1679,8 +1910,14 @@ function buildDispatchScript(messageLiteral) {
1679
1910
  "})();"
1680
1911
  ].join("\n");
1681
1912
  }
1682
- function buildFilesPickedScript(files, batchId) {
1683
- return buildDispatchScript(`{ type: "sagepilot:files_picked", batch_id: ${JSON.stringify(batchId)}, files: ${JSON.stringify(files)} }`);
1913
+ function buildFilesPickedScript(batch) {
1914
+ return buildDispatchScript(JSON.stringify({
1915
+ type: "sagepilot:files_picked",
1916
+ batch_id: batch.batchId,
1917
+ chat_id: batch.target?.chatId,
1918
+ conversation_id: batch.target?.chatId,
1919
+ files: batch.files
1920
+ }));
1684
1921
  }
1685
1922
  function buildFilePickerErrorScript(message, code) {
1686
1923
  return buildDispatchScript(
@@ -1727,7 +1964,7 @@ function isInternalWebViewScheme(url) {
1727
1964
  var hostedChatWebViewProps = {
1728
1965
  allowFileAccess: true,
1729
1966
  allowFileAccessFromFileURLs: true,
1730
- ...import_react_native2.Platform.OS === "ios" ? {
1967
+ ...import_react_native3.Platform.OS === "ios" ? {
1731
1968
  automaticallyAdjustContentInsets: false,
1732
1969
  contentInsetAdjustmentBehavior: "never",
1733
1970
  hideKeyboardAccessoryView: true
@@ -1742,7 +1979,7 @@ var hostedChatWebViewProps = {
1742
1979
  sharedCookiesEnabled: true,
1743
1980
  thirdPartyCookiesEnabled: true
1744
1981
  };
1745
- var AndroidInsetsView = import_react_native2.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native2.View;
1982
+ var AndroidInsetsView = import_react_native3.Platform.OS === "android" ? SagepilotInsetsViewNativeComponent_default : import_react_native3.View;
1746
1983
  var emptyAndroidInsets = { top: 0, bottom: 0 };
1747
1984
  function SagepilotChatProvider({
1748
1985
  children,
@@ -1761,8 +1998,11 @@ function SagepilotChatProvider({
1761
1998
  const deliveryAttemptsRef = (0, import_react.useRef)(0);
1762
1999
  const pendingPingRef = (0, import_react.useRef)(null);
1763
2000
  const pingTimeoutRef = (0, import_react.useRef)(null);
2001
+ const livenessResumeTimerRef = (0, import_react.useRef)(null);
2002
+ const livenessPausedUntilRef = (0, import_react.useRef)(0);
2003
+ const pickerInFlightRef = (0, import_react.useRef)(false);
1764
2004
  const livenessRemountCountRef = (0, import_react.useRef)(0);
1765
- const appStateRef = (0, import_react.useRef)(import_react_native2.AppState.currentState);
2005
+ const appStateRef = (0, import_react.useRef)(import_react_native3.AppState.currentState);
1766
2006
  const didReconcileRef = (0, import_react.useRef)(false);
1767
2007
  const [androidRepaintTick, setAndroidRepaintTick] = (0, import_react.useState)(0);
1768
2008
  const [sourceChooser, setSourceChooser] = (0, import_react.useState)(null);
@@ -1783,6 +2023,7 @@ function SagepilotChatProvider({
1783
2023
  if (hasFileStore) {
1784
2024
  const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
1785
2025
  batchId: batch.batchId,
2026
+ target: batch.target,
1786
2027
  files: batch.files.map((file, index) => ({
1787
2028
  file_name: file.file_name,
1788
2029
  mime_type: file.mime_type,
@@ -1801,6 +2042,7 @@ function SagepilotChatProvider({
1801
2042
  if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
1802
2043
  const manifest = queue.map((batch) => ({
1803
2044
  batchId: batch.batchId,
2045
+ target: batch.target,
1804
2046
  files: batch.files.map((file) => ({
1805
2047
  file_name: file.file_name,
1806
2048
  mime_type: file.mime_type,
@@ -1842,12 +2084,17 @@ function SagepilotChatProvider({
1842
2084
  }
1843
2085
  const batch = pendingBatchesRef.current[0];
1844
2086
  if (!batch) return;
2087
+ if (internalSagepilotChat.restoreHostedAttachmentTarget(batch.target)) {
2088
+ widgetReadyRef.current = false;
2089
+ deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
2090
+ return;
2091
+ }
1845
2092
  deliveryAttemptsRef.current += 1;
1846
2093
  const attempts = deliveryAttemptsRef.current;
1847
2094
  const ref = nativeWebViewRef.current;
1848
2095
  const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
1849
2096
  if (shouldDeliver && ref) {
1850
- ref.injectJavaScript(buildFilesPickedScript(batch.files, batch.batchId));
2097
+ ref.injectJavaScript(buildFilesPickedScript(batch));
1851
2098
  }
1852
2099
  if (attempts < DELIVERY_MAX_ATTEMPTS) {
1853
2100
  deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
@@ -1878,22 +2125,38 @@ function SagepilotChatProvider({
1878
2125
  const batchId = nextBatchId();
1879
2126
  const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
1880
2127
  const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
1881
- const batch = { batchId, files, storageKeys };
2128
+ const batch = {
2129
+ batchId,
2130
+ files,
2131
+ storageKeys,
2132
+ target: internalSagepilotChat.getHostedAttachmentTarget()
2133
+ };
1882
2134
  pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
1883
2135
  ensureDelivery();
1884
2136
  writeManifest();
1885
2137
  void writeBatchBytes(batch);
1886
2138
  }, [ensureDelivery, writeManifest, writeBatchBytes]);
1887
- const recoverNativeWebView = (0, import_react.useCallback)(() => {
2139
+ const recoverNativeWebView = (0, import_react.useCallback)((_reasonOrEvent) => {
1888
2140
  widgetReadyRef.current = false;
1889
2141
  setNativeWebViewKey((key) => key + 1);
1890
2142
  }, []);
1891
2143
  const recoverPreloadWebView = (0, import_react.useCallback)(() => {
1892
2144
  setPreloadWebViewKey((key) => key + 1);
1893
2145
  }, []);
2146
+ const pauseLivenessAfterPicker = (0, import_react.useCallback)((durationMs) => {
2147
+ if (import_react_native3.Platform.OS !== "android") return;
2148
+ livenessPausedUntilRef.current = Math.max(livenessPausedUntilRef.current, Date.now() + durationMs);
2149
+ }, []);
1894
2150
  const runNativeFilePicker = (0, import_react.useCallback)((source, multiple) => {
1895
2151
  const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1896
- if (!filePicker) return;
2152
+ if (!filePicker) {
2153
+ return;
2154
+ }
2155
+ if (pickerInFlightRef.current) {
2156
+ return;
2157
+ }
2158
+ pauseLivenessAfterPicker(6e4);
2159
+ pickerInFlightRef.current = true;
1897
2160
  filePicker.pickFiles({ source, multiple }).then((files) => {
1898
2161
  if (files.length === 0) {
1899
2162
  nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
@@ -1903,13 +2166,18 @@ function SagepilotChatProvider({
1903
2166
  }).catch((error) => {
1904
2167
  const { message, code } = readFilePickerError(error);
1905
2168
  nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
2169
+ }).finally(() => {
2170
+ pickerInFlightRef.current = false;
2171
+ pauseLivenessAfterPicker(LIVENESS_PICKER_GRACE_MS);
1906
2172
  });
1907
- }, [queuePickedFiles]);
2173
+ }, [pauseLivenessAfterPicker, queuePickedFiles]);
1908
2174
  const openNativeFilePicker = (0, import_react.useCallback)((multiple) => {
1909
2175
  const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1910
- if (!filePicker || filePicker.sources.length === 0) return;
2176
+ if (!filePicker || filePicker.sources.length === 0) {
2177
+ return;
2178
+ }
1911
2179
  const [onlySource] = filePicker.sources;
1912
- if (filePicker.sources.length === 1 && onlySource) {
2180
+ if (filePicker.sources.length === 1 && onlySource && onlySource !== "camera") {
1913
2181
  runNativeFilePicker(onlySource, multiple);
1914
2182
  return;
1915
2183
  }
@@ -1962,7 +2230,8 @@ function SagepilotChatProvider({
1962
2230
  restored.push({
1963
2231
  batchId: batch.batchId,
1964
2232
  files,
1965
- storageKeys: storageKeys.length === files.length ? storageKeys : void 0
2233
+ storageKeys: storageKeys.length === files.length ? storageKeys : void 0,
2234
+ target: batch.target
1966
2235
  });
1967
2236
  }
1968
2237
  }
@@ -1983,6 +2252,19 @@ function SagepilotChatProvider({
1983
2252
  cancelled = true;
1984
2253
  };
1985
2254
  }, [getPendingCache, startDelivery, writeManifest]);
2255
+ const clearPing = (0, import_react.useCallback)(() => {
2256
+ pendingPingRef.current = null;
2257
+ if (pingTimeoutRef.current) {
2258
+ clearTimeout(pingTimeoutRef.current);
2259
+ pingTimeoutRef.current = null;
2260
+ }
2261
+ }, []);
2262
+ const clearDeferredLivenessProbe = (0, import_react.useCallback)(() => {
2263
+ if (livenessResumeTimerRef.current) {
2264
+ clearTimeout(livenessResumeTimerRef.current);
2265
+ livenessResumeTimerRef.current = null;
2266
+ }
2267
+ }, []);
1986
2268
  (0, import_react.useEffect)(() => {
1987
2269
  return () => {
1988
2270
  if (deliveryTimerRef.current) {
@@ -1993,19 +2275,23 @@ function SagepilotChatProvider({
1993
2275
  clearTimeout(pingTimeoutRef.current);
1994
2276
  pingTimeoutRef.current = null;
1995
2277
  }
2278
+ clearDeferredLivenessProbe();
1996
2279
  };
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
- }, []);
2280
+ }, [clearDeferredLivenessProbe]);
2005
2281
  const runLivenessProbe = (0, import_react.useCallback)(() => {
2006
- if (import_react_native2.Platform.OS !== "android") return;
2282
+ if (import_react_native3.Platform.OS !== "android") return;
2007
2283
  const ref = nativeWebViewRef.current;
2008
2284
  if (!ref || !internalSagepilotChat.isPresented()) return;
2285
+ const pauseRemainingMs = livenessPausedUntilRef.current - Date.now();
2286
+ if (pickerInFlightRef.current || pauseRemainingMs > 0) {
2287
+ const retryDelayMs = pickerInFlightRef.current ? 500 : Math.max(0, pauseRemainingMs);
2288
+ clearDeferredLivenessProbe();
2289
+ livenessResumeTimerRef.current = setTimeout(() => {
2290
+ livenessResumeTimerRef.current = null;
2291
+ runLivenessProbe();
2292
+ }, retryDelayMs);
2293
+ return;
2294
+ }
2009
2295
  setAndroidRepaintTick((tick) => tick + 1);
2010
2296
  const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
2011
2297
  const nonce = `${Date.now().toString(36)}_${attempts}`;
@@ -2015,14 +2301,14 @@ function SagepilotChatProvider({
2015
2301
  pingTimeoutRef.current = setTimeout(() => {
2016
2302
  if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
2017
2303
  clearPing();
2018
- recoverNativeWebView();
2304
+ recoverNativeWebView("liveness_timeout");
2019
2305
  return;
2020
2306
  }
2021
2307
  runLivenessProbe();
2022
2308
  }, LIVENESS_PONG_TIMEOUT_MS);
2023
- }, [clearPing, recoverNativeWebView]);
2309
+ }, [clearDeferredLivenessProbe, clearPing, recoverNativeWebView]);
2024
2310
  (0, import_react.useEffect)(() => {
2025
- const subscription = import_react_native2.AppState.addEventListener("change", (nextState) => {
2311
+ const subscription = import_react_native3.AppState.addEventListener("change", (nextState) => {
2026
2312
  const prev = appStateRef.current;
2027
2313
  appStateRef.current = nextState;
2028
2314
  if (nextState === "active" && (prev === "background" || prev === "inactive")) {
@@ -2050,11 +2336,11 @@ function SagepilotChatProvider({
2050
2336
  const presentationStyle = state.presentation?.style ?? "sheet";
2051
2337
  const isFullScreenModal = presentationStyle === "fullScreen";
2052
2338
  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" ? {
2339
+ const ModalContainer = import_react_native3.Platform.OS === "ios" && isFullScreenModal ? import_react_native3.SafeAreaView : import_react_native3.View;
2340
+ const NativeModalContainer = import_react_native3.Platform.OS === "android" ? AndroidInsetsView : ModalContainer;
2341
+ const ChatContentContainer = import_react_native3.Platform.OS === "ios" ? import_react_native3.KeyboardAvoidingView : import_react_native3.View;
2342
+ const nativeModalContainerProps = import_react_native3.Platform.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
2343
+ const chatContentContainerProps = import_react_native3.Platform.OS === "ios" ? {
2058
2344
  behavior: "padding",
2059
2345
  enabled: true,
2060
2346
  keyboardVerticalOffset: 0,
@@ -2062,7 +2348,7 @@ function SagepilotChatProvider({
2062
2348
  } : {
2063
2349
  style: [
2064
2350
  styles.modalContent,
2065
- import_react_native2.Platform.OS === "android" ? {
2351
+ import_react_native3.Platform.OS === "android" ? {
2066
2352
  paddingTop: androidModalInsets.top,
2067
2353
  paddingBottom: androidModalInsets.bottom
2068
2354
  } : null
@@ -2094,7 +2380,7 @@ function SagepilotChatProvider({
2094
2380
  clearPing();
2095
2381
  if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
2096
2382
  livenessRemountCountRef.current += 1;
2097
- recoverNativeWebView();
2383
+ recoverNativeWebView("liveness_dead_pong");
2098
2384
  }
2099
2385
  } else {
2100
2386
  livenessRemountCountRef.current = 0;
@@ -2116,7 +2402,12 @@ function SagepilotChatProvider({
2116
2402
  if (!script || !nativeWebViewRef.current) return;
2117
2403
  nativeWebViewRef.current.injectJavaScript(script);
2118
2404
  };
2405
+ const injectNativeBridgeToNativeWebView = () => {
2406
+ if (!nativeWebViewRef.current) return;
2407
+ nativeWebViewRef.current.injectJavaScript(getInjectedWebViewScript());
2408
+ };
2119
2409
  const handleNativeWebViewLoadEnd = () => {
2410
+ injectNativeBridgeToNativeWebView();
2120
2411
  postIdentityToNativeWebView();
2121
2412
  widgetReadyRef.current = false;
2122
2413
  startDelivery();
@@ -2130,11 +2421,11 @@ function SagepilotChatProvider({
2130
2421
  if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
2131
2422
  return true;
2132
2423
  }
2133
- import_react_native2.Linking.openURL(url).catch(() => void 0);
2424
+ import_react_native3.Linking.openURL(url).catch(() => void 0);
2134
2425
  return false;
2135
2426
  }, [state.conversationUrl, state.preloadUrl]);
2136
2427
  (0, import_react.useEffect)(() => {
2137
- if (import_react_native2.Platform.OS !== "web" || typeof window === "undefined") return;
2428
+ if (import_react_native3.Platform.OS !== "web" || typeof window === "undefined") return;
2138
2429
  const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
2139
2430
  if (!trustedWidgetOrigin) return;
2140
2431
  const handleWindowMessage = (event) => {
@@ -2145,16 +2436,16 @@ function SagepilotChatProvider({
2145
2436
  return () => window.removeEventListener("message", handleWindowMessage);
2146
2437
  }, [state.conversationUrl]);
2147
2438
  (0, import_react.useEffect)(() => {
2148
- if (import_react_native2.Platform.OS !== "web" || !state.isPresented) return;
2439
+ if (import_react_native3.Platform.OS !== "web" || !state.isPresented) return;
2149
2440
  postIdentityToWebFrame();
2150
2441
  }, [state.isPresented, state.conversationUrl, state.configured]);
2151
2442
  (0, import_react.useEffect)(() => {
2152
- if (import_react_native2.Platform.OS === "web" || !state.isPresented) return;
2443
+ if (import_react_native3.Platform.OS === "web" || !state.isPresented) return;
2153
2444
  postIdentityToNativeWebView();
2154
2445
  }, [state.isPresented, state.conversationUrl, state.configured]);
2155
- if (import_react_native2.Platform.OS === "web") {
2446
+ if (import_react_native3.Platform.OS === "web") {
2156
2447
  return (0, import_react.createElement)(
2157
- import_react_native2.View,
2448
+ import_react_native3.View,
2158
2449
  { style: styles.root },
2159
2450
  children,
2160
2451
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)("iframe", {
@@ -2163,20 +2454,20 @@ function SagepilotChatProvider({
2163
2454
  title: "Sagepilot chat preload"
2164
2455
  }) : null,
2165
2456
  state.configured && state.isPresented && state.conversationUrl ? (0, import_react.createElement)(
2166
- import_react_native2.View,
2457
+ import_react_native3.View,
2167
2458
  { style: styles.webOverlay },
2168
2459
  (0, import_react.createElement)(
2169
- import_react_native2.View,
2460
+ import_react_native3.View,
2170
2461
  { style: styles.webPanel },
2171
2462
  showCloseButton ? (0, import_react.createElement)(
2172
- import_react_native2.Pressable,
2463
+ import_react_native3.Pressable,
2173
2464
  {
2174
2465
  accessibilityRole: "button",
2175
2466
  accessibilityLabel: closeLabel,
2176
2467
  onPress: () => internalSagepilotChat.dismiss(),
2177
2468
  style: styles.webCloseButton
2178
2469
  },
2179
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.closeText }, closeLabel)
2470
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.closeText }, closeLabel)
2180
2471
  ) : null,
2181
2472
  (0, import_react.createElement)("iframe", {
2182
2473
  ref: webFrameRef,
@@ -2190,7 +2481,7 @@ function SagepilotChatProvider({
2190
2481
  );
2191
2482
  }
2192
2483
  return (0, import_react.createElement)(
2193
- import_react_native2.View,
2484
+ import_react_native3.View,
2194
2485
  { style: styles.root },
2195
2486
  children,
2196
2487
  state.configured && !state.isPresented && state.shouldPreload && state.preloadUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
@@ -2209,13 +2500,13 @@ function SagepilotChatProvider({
2209
2500
  onContentProcessDidTerminate: recoverPreloadWebView
2210
2501
  }) : null,
2211
2502
  (0, import_react.createElement)(
2212
- import_react_native2.Modal,
2503
+ import_react_native3.Modal,
2213
2504
  {
2214
2505
  visible: state.configured && state.isPresented,
2215
2506
  animationType,
2216
2507
  presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
2217
- statusBarTranslucent: import_react_native2.Platform.OS === "android",
2218
- navigationBarTranslucent: import_react_native2.Platform.OS === "android",
2508
+ statusBarTranslucent: import_react_native3.Platform.OS === "android",
2509
+ navigationBarTranslucent: import_react_native3.Platform.OS === "android",
2219
2510
  onRequestClose: () => internalSagepilotChat.dismiss()
2220
2511
  },
2221
2512
  (0, import_react.createElement)(
@@ -2225,17 +2516,17 @@ function SagepilotChatProvider({
2225
2516
  ChatContentContainer,
2226
2517
  chatContentContainerProps,
2227
2518
  showCloseButton ? (0, import_react.createElement)(
2228
- import_react_native2.View,
2519
+ import_react_native3.View,
2229
2520
  { style: styles.header },
2230
2521
  (0, import_react.createElement)(
2231
- import_react_native2.Pressable,
2522
+ import_react_native3.Pressable,
2232
2523
  {
2233
2524
  accessibilityRole: "button",
2234
2525
  accessibilityLabel: closeLabel,
2235
2526
  onPress: () => internalSagepilotChat.dismiss(),
2236
2527
  style: styles.closeButton
2237
2528
  },
2238
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.closeText }, closeLabel)
2529
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.closeText }, closeLabel)
2239
2530
  )
2240
2531
  ) : null,
2241
2532
  state.conversationUrl ? (0, import_react.createElement)(import_react_native_webview.WebView, {
@@ -2246,7 +2537,7 @@ function SagepilotChatProvider({
2246
2537
  // The imperceptible opacity toggle forces the Android WebView
2247
2538
  // surface to re-composite on resume, clearing the blank-but-alive
2248
2539
  // 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,
2540
+ style: import_react_native3.Platform.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
2250
2541
  startInLoadingState: true,
2251
2542
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
2252
2543
  onMessage: handleWebViewMessage,
@@ -2258,10 +2549,10 @@ function SagepilotChatProvider({
2258
2549
  // iOS equivalent: WKWebView content process terminated.
2259
2550
  onContentProcessDidTerminate: recoverNativeWebView,
2260
2551
  renderLoading: () => (0, import_react.createElement)(
2261
- import_react_native2.View,
2552
+ import_react_native3.View,
2262
2553
  { 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)
2554
+ (0, import_react.createElement)(import_react_native3.ActivityIndicator, null),
2555
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.loadingText }, loadingLabel)
2265
2556
  )
2266
2557
  }) : null
2267
2558
  )
@@ -2270,7 +2561,7 @@ function SagepilotChatProvider({
2270
2561
  // Native attachment-source chooser. Replaces the Android Alert (capped at
2271
2562
  // 3 buttons) so camera/library/documents all show on every platform.
2272
2563
  (0, import_react.createElement)(
2273
- import_react_native2.Modal,
2564
+ import_react_native3.Modal,
2274
2565
  {
2275
2566
  visible: sourceChooser !== null,
2276
2567
  transparent: true,
@@ -2279,39 +2570,50 @@ function SagepilotChatProvider({
2279
2570
  onRequestClose: () => setSourceChooser(null)
2280
2571
  },
2281
2572
  (0, import_react.createElement)(
2282
- import_react_native2.Pressable,
2573
+ import_react_native3.Pressable,
2283
2574
  { style: styles.sheetBackdrop, onPress: () => setSourceChooser(null) },
2284
2575
  (0, import_react.createElement)(
2285
- import_react_native2.View,
2576
+ import_react_native3.View,
2286
2577
  { style: styles.sheetCard },
2287
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetTitle }, "Add attachment"),
2578
+ (0, import_react.createElement)(import_react_native3.View, { style: styles.sheetHandle }),
2579
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetTitle }, "Add attachment"),
2288
2580
  ...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
2289
2581
  (source) => (0, import_react.createElement)(
2290
- import_react_native2.Pressable,
2582
+ import_react_native3.Pressable,
2291
2583
  {
2292
2584
  key: source,
2293
2585
  accessibilityRole: "button",
2294
- style: styles.sheetButton,
2586
+ android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
2587
+ hitSlop: 4,
2588
+ style: ({ pressed }) => getSheetOptionStyle(pressed),
2295
2589
  onPress: () => handleSourceChoice(source)
2296
2590
  },
2297
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source])
2591
+ renderFilePickerSourceIcon(source),
2592
+ (0, import_react.createElement)(
2593
+ import_react_native3.View,
2594
+ { style: styles.sheetOptionText },
2595
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source]),
2596
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetButtonDescription }, FILE_PICKER_SOURCE_DESCRIPTIONS[source])
2597
+ )
2298
2598
  )
2299
2599
  ),
2300
2600
  (0, import_react.createElement)(
2301
- import_react_native2.Pressable,
2601
+ import_react_native3.Pressable,
2302
2602
  {
2303
2603
  accessibilityRole: "button",
2304
- style: [styles.sheetButton, styles.sheetCancelButton],
2604
+ android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
2605
+ style: ({ pressed }) => getSheetCancelStyle(pressed),
2305
2606
  onPress: () => setSourceChooser(null)
2306
2607
  },
2307
- (0, import_react.createElement)(import_react_native2.Text, { style: styles.sheetCancelText }, "Cancel")
2608
+ (0, import_react.createElement)(import_lucide_react_native.X, { color: "#334155", size: 18, strokeWidth: 2.5, style: styles.sheetCancelIcon }),
2609
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.sheetCancelText }, "Cancel")
2308
2610
  )
2309
2611
  )
2310
2612
  )
2311
2613
  )
2312
2614
  );
2313
2615
  }
2314
- var styles = import_react_native2.StyleSheet.create({
2616
+ var styles = import_react_native3.StyleSheet.create({
2315
2617
  root: {
2316
2618
  flex: 1
2317
2619
  },
@@ -2397,7 +2699,7 @@ var styles = import_react_native2.StyleSheet.create({
2397
2699
  borderStyle: "none"
2398
2700
  },
2399
2701
  loading: {
2400
- ...import_react_native2.StyleSheet.absoluteFillObject,
2702
+ ...import_react_native3.StyleSheet.absoluteFillObject,
2401
2703
  alignItems: "center",
2402
2704
  justifyContent: "center",
2403
2705
  backgroundColor: "#ffffff"
@@ -2409,43 +2711,109 @@ var styles = import_react_native2.StyleSheet.create({
2409
2711
  },
2410
2712
  sheetBackdrop: {
2411
2713
  flex: 1,
2714
+ alignItems: "center",
2412
2715
  justifyContent: "flex-end",
2413
- backgroundColor: "rgba(17, 24, 39, 0.36)"
2716
+ backgroundColor: "rgba(15, 23, 42, 0.48)"
2414
2717
  },
2415
2718
  sheetCard: {
2719
+ width: "100%",
2720
+ maxWidth: 540,
2416
2721
  backgroundColor: "#ffffff",
2417
- borderTopLeftRadius: 16,
2418
- borderTopRightRadius: 16,
2419
- paddingTop: 8,
2420
- paddingBottom: 24,
2421
- paddingHorizontal: 8
2722
+ borderTopLeftRadius: 26,
2723
+ borderTopRightRadius: 26,
2724
+ paddingTop: 10,
2725
+ paddingBottom: 18,
2726
+ paddingHorizontal: 14,
2727
+ shadowColor: "#0f172a",
2728
+ shadowOpacity: 0.22,
2729
+ shadowRadius: 28,
2730
+ shadowOffset: { width: 0, height: -10 },
2731
+ elevation: 18
2732
+ },
2733
+ sheetHandle: {
2734
+ alignSelf: "center",
2735
+ width: 42,
2736
+ height: 5,
2737
+ borderRadius: 999,
2738
+ backgroundColor: "#d1d5db",
2739
+ marginBottom: 14
2422
2740
  },
2423
2741
  sheetTitle: {
2424
- textAlign: "center",
2425
- color: "#6b7280",
2426
- fontSize: 13,
2427
- fontWeight: "600",
2428
- paddingVertical: 10
2742
+ color: "#111827",
2743
+ fontSize: 17,
2744
+ fontWeight: "700",
2745
+ paddingBottom: 12,
2746
+ paddingHorizontal: 4
2747
+ },
2748
+ sheetOption: {
2749
+ minHeight: 70,
2750
+ flexDirection: "row",
2751
+ alignItems: "center",
2752
+ borderRadius: 18,
2753
+ paddingHorizontal: 12,
2754
+ marginBottom: 6,
2755
+ backgroundColor: "#ffffff"
2756
+ },
2757
+ sheetOptionPressed: {
2758
+ backgroundColor: "#f8fafc",
2759
+ transform: [{ scale: 0.985 }]
2429
2760
  },
2430
- sheetButton: {
2431
- minHeight: 52,
2761
+ sheetOptionIcon: {
2762
+ width: 46,
2763
+ height: 46,
2764
+ borderRadius: 16,
2765
+ marginRight: 14,
2432
2766
  alignItems: "center",
2433
2767
  justifyContent: "center",
2434
- borderRadius: 12
2768
+ position: "relative",
2769
+ overflow: "hidden"
2770
+ },
2771
+ sheetOptionText: {
2772
+ flex: 1,
2773
+ justifyContent: "center"
2435
2774
  },
2436
2775
  sheetButtonText: {
2437
- color: "#111827",
2776
+ color: "#0f172a",
2438
2777
  fontSize: 16,
2439
- fontWeight: "500"
2778
+ fontWeight: "700"
2779
+ },
2780
+ sheetButtonDescription: {
2781
+ color: "#64748b",
2782
+ fontSize: 13,
2783
+ fontWeight: "500",
2784
+ marginTop: 3
2785
+ },
2786
+ cameraOptionIcon: {
2787
+ backgroundColor: "#e0f2fe"
2788
+ },
2789
+ libraryOptionIcon: {
2790
+ backgroundColor: "#ecfdf5"
2791
+ },
2792
+ documentsOptionIcon: {
2793
+ backgroundColor: "#f5f3ff"
2440
2794
  },
2441
2795
  sheetCancelButton: {
2442
- marginTop: 6,
2443
- backgroundColor: "#f3f4f6"
2796
+ minHeight: 56,
2797
+ flexDirection: "row",
2798
+ alignItems: "center",
2799
+ justifyContent: "center",
2800
+ borderRadius: 18,
2801
+ marginTop: 8,
2802
+ backgroundColor: "#f1f5f9"
2803
+ },
2804
+ sheetCancelButtonPressed: {
2805
+ backgroundColor: "#e2e8f0",
2806
+ transform: [{ scale: 0.985 }]
2807
+ },
2808
+ sheetCancelIcon: {
2809
+ width: 18,
2810
+ height: 18,
2811
+ marginRight: 8
2444
2812
  },
2445
2813
  sheetCancelText: {
2446
- color: "#111827",
2814
+ color: "#0f172a",
2447
2815
  fontSize: 16,
2448
- fontWeight: "600"
2816
+ fontWeight: "700"
2449
2817
  }
2450
2818
  });
2451
2819
 
@@ -2500,6 +2868,9 @@ function useSagepilotChat() {
2500
2868
  }
2501
2869
  // Annotate the CommonJS export names for ESM import in node:
2502
2870
  0 && (module.exports = {
2871
+ SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
2872
+ SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
2873
+ SAGEPILOT_DEFAULT_IMAGE_QUALITY,
2503
2874
  SagepilotChat,
2504
2875
  SagepilotChatError,
2505
2876
  SagepilotChatProvider,
@@ -2508,5 +2879,7 @@ function useSagepilotChat() {
2508
2879
  createKeychainTokenStorage,
2509
2880
  createSagepilotFilePicker,
2510
2881
  createSagepilotFileStore,
2882
+ ensureSagepilotAndroidCameraPermission,
2883
+ promptOpenSagepilotCameraSettings,
2511
2884
  useSagepilotChat
2512
2885
  });