@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.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ // src/public/SdkClient.ts
2
+ import { Platform } from "react-native";
3
+
1
4
  // src/core/errors/SagepilotChatError.ts
2
5
  var SagepilotChatError = class extends Error {
3
6
  constructor(code, message, options = {}) {
@@ -15,7 +18,7 @@ var SagepilotChatError = class extends Error {
15
18
 
16
19
  // src/core/config/constants.ts
17
20
  var SDK_NAME = "@sagepilot-ai/react-native-sdk";
18
- var SDK_VERSION = "0.2.5";
21
+ var SDK_VERSION = "0.3.0";
19
22
  var DEFAULT_HOST = "https://app.sagepilot.ai";
20
23
  var DEFAULT_WIDGET_HOST = "https://app.sagepilot.ai";
21
24
  var CUSTOMER_API_PREFIX = "/customer-api/v1";
@@ -324,6 +327,34 @@ async function resolveDeviceInfo(adapter) {
324
327
  return adapter.getDeviceInfo();
325
328
  }
326
329
 
330
+ // src/core/storage/cache.ts
331
+ function createAsyncStorageCacheStorage(asyncStorage) {
332
+ return {
333
+ getItem: (key) => asyncStorage.getItem(key),
334
+ setItem: (key, value) => asyncStorage.setItem(key, value),
335
+ removeItem: (key) => asyncStorage.removeItem(key)
336
+ };
337
+ }
338
+ function createJsonCache(storage, namespace) {
339
+ return {
340
+ async get(key) {
341
+ const value = await storage.getItem(`${namespace}:${key}`);
342
+ if (!value) return null;
343
+ try {
344
+ return JSON.parse(value);
345
+ } catch {
346
+ return null;
347
+ }
348
+ },
349
+ async set(key, value) {
350
+ await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
351
+ },
352
+ async remove(key) {
353
+ await storage.removeItem(`${namespace}:${key}`);
354
+ }
355
+ };
356
+ }
357
+
327
358
  // src/resources/channels.ts
328
359
  function bootstrapChannel(http, host, channelId) {
329
360
  return http.request(
@@ -407,6 +438,9 @@ function fetchUnreadCount(http, host, channelId, sessionId, authorizationHeader)
407
438
  }
408
439
 
409
440
  // src/public/SdkClient.ts
441
+ var PRESENTATION_CACHE_NAMESPACE = "sagepilot_rn_presentation";
442
+ var PRESENTATION_CACHE_KEY = "state";
443
+ var PRESENTATION_RESTORE_MAX_AGE_MS = 15 * 60 * 1e3;
410
444
  var DEFAULT_MOBILE_LAUNCHER_CONFIG = {
411
445
  label: "Chat",
412
446
  buttonColor: "#173c2d",
@@ -476,6 +510,10 @@ function readRecordField(input, key) {
476
510
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
477
511
  return value;
478
512
  }
513
+ function isFreshPresentationState(updatedAt) {
514
+ if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) return false;
515
+ return Date.now() - updatedAt <= PRESENTATION_RESTORE_MAX_AGE_MS;
516
+ }
479
517
  function toPublicSessionState(session) {
480
518
  return {
481
519
  session_id: session.session_id,
@@ -565,6 +603,7 @@ var SagepilotReactNativeChat = class {
565
603
  this.channel = channel;
566
604
  this.session = session;
567
605
  this.isConfigured = true;
606
+ await this.restorePresentationFromCache();
568
607
  this.emitReady();
569
608
  this.emitState();
570
609
  if (config.behavior?.enableUnreadPolling ?? true) {
@@ -704,12 +743,14 @@ var SagepilotReactNativeChat = class {
704
743
  this.hostedChatView = { screen: "home" };
705
744
  if (this.presented) {
706
745
  this.emitState();
746
+ this.persistPresentationState();
707
747
  return true;
708
748
  }
709
749
  this.presented = true;
710
750
  this.setUnreadCount(0);
711
751
  this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
712
752
  this.emitState();
753
+ this.persistPresentationState();
713
754
  return true;
714
755
  }
715
756
  presentMessages() {
@@ -737,6 +778,7 @@ var SagepilotReactNativeChat = class {
737
778
  this.emitUnreadChange();
738
779
  this.dismissCallbacks.forEach((callback) => callback(this.getLifecycleState()));
739
780
  this.emitState();
781
+ this.persistPresentationState();
740
782
  return true;
741
783
  }
742
784
  hide() {
@@ -871,6 +913,10 @@ var SagepilotReactNativeChat = class {
871
913
  }
872
914
  return this.session?.conversation_id ?? void 0;
873
915
  }
916
+ /** Returns the chat id that is explicitly part of the current hosted URL. */
917
+ getHostedRouteChatId() {
918
+ return this.hostedChatView.screen === "composer" ? this.hostedChatView.chatId : void 0;
919
+ }
874
920
  getHostedIdentityMessage() {
875
921
  if (!this.legacyWidgetJwt) return null;
876
922
  return {
@@ -881,6 +927,34 @@ var SagepilotReactNativeChat = class {
881
927
  }
882
928
  };
883
929
  }
930
+ /** Captures the hosted route that should receive a picked attachment batch. */
931
+ getHostedAttachmentTarget() {
932
+ return {
933
+ chatId: this.getActiveHostedChatId(),
934
+ routeChatId: this.getHostedRouteChatId(),
935
+ hostedChatView: this.serializeHostedChatView(this.hostedChatView)
936
+ };
937
+ }
938
+ /** Restores the hosted route for a queued attachment batch before delivery. */
939
+ restoreHostedAttachmentTarget(target) {
940
+ if (Platform.OS !== "android") return false;
941
+ if (!target) return false;
942
+ const hostedChatView = this.readPersistedHostedChatView(target.hostedChatView);
943
+ if (!hostedChatView) return false;
944
+ const currentChatId = this.getActiveHostedChatId();
945
+ const targetChatId = normalizeOptionalString(target.chatId);
946
+ const routeChatId = normalizeOptionalString(target.routeChatId) ?? (hostedChatView.screen === "composer" ? normalizeOptionalString(hostedChatView.chatId) : void 0);
947
+ const nextHostedChatView = routeChatId && hostedChatView.screen === "composer" ? this.withPinnedChatId(hostedChatView, routeChatId) : hostedChatView;
948
+ if (this.presented && (!targetChatId || currentChatId === targetChatId) && this.areHostedChatViewsEqual(this.hostedChatView, nextHostedChatView)) {
949
+ return false;
950
+ }
951
+ this.hostedChatView = nextHostedChatView;
952
+ this.presented = true;
953
+ this.setUnreadCount(0);
954
+ this.emitState();
955
+ this.persistPresentationState();
956
+ return true;
957
+ }
884
958
  handleHostedBridgeMessage(message) {
885
959
  if (!message) return false;
886
960
  if (message.type === "close_widget") {
@@ -948,6 +1022,7 @@ var SagepilotReactNativeChat = class {
948
1022
  }
949
1023
  destroy() {
950
1024
  this.stopUnreadPolling();
1025
+ this.persistPresentationState({ cleanShutdown: true, presented: false });
951
1026
  this.resetRuntimeState();
952
1027
  this.emitState();
953
1028
  this.identifyCallbacks.clear();
@@ -1103,8 +1178,86 @@ var SagepilotReactNativeChat = class {
1103
1178
  this.presentCallbacks.forEach((callback) => callback(this.getLifecycleState()));
1104
1179
  }
1105
1180
  this.emitState();
1181
+ this.persistPresentationState();
1106
1182
  return true;
1107
1183
  }
1184
+ /** Returns the dedicated cache used for hosted-chat presentation recovery. */
1185
+ getPresentationCache() {
1186
+ const cacheStorage = this.runtimeOptions?.cacheStorage;
1187
+ if (!cacheStorage) return null;
1188
+ return createJsonCache(cacheStorage, PRESENTATION_CACHE_NAMESPACE);
1189
+ }
1190
+ /** Removes callback-only fields so the hosted route can be safely serialized. */
1191
+ serializeHostedChatView(view) {
1192
+ if (view.screen !== "composer") return view;
1193
+ return {
1194
+ screen: "composer",
1195
+ message: view.message,
1196
+ mode: view.mode,
1197
+ chatId: view.chatId,
1198
+ metadata: view.metadata
1199
+ };
1200
+ }
1201
+ /** Validates a persisted hosted view before using it to rebuild a widget URL. */
1202
+ readPersistedHostedChatView(view) {
1203
+ if (!view || typeof view !== "object") return null;
1204
+ const record = view;
1205
+ if (record.screen === "home") return { screen: "home" };
1206
+ if (record.screen === "messages") return { screen: "messages" };
1207
+ if (record.screen !== "composer") return null;
1208
+ const mode = record.mode === "new" ? "new" : "auto";
1209
+ return {
1210
+ screen: "composer",
1211
+ message: typeof record.message === "string" ? record.message : void 0,
1212
+ mode,
1213
+ chatId: typeof record.chatId === "string" && record.chatId ? record.chatId : void 0,
1214
+ metadata: readRecordField(record, "metadata")
1215
+ };
1216
+ }
1217
+ /** Forces a persisted hosted route to target the exact chat that owns a pending attachment. */
1218
+ withPinnedChatId(view, chatId) {
1219
+ if (view.screen === "composer") {
1220
+ return {
1221
+ ...view,
1222
+ chatId
1223
+ };
1224
+ }
1225
+ return {
1226
+ screen: "composer",
1227
+ mode: "auto",
1228
+ chatId
1229
+ };
1230
+ }
1231
+ /** Compares hosted route state without relying on object identity. */
1232
+ areHostedChatViewsEqual(first, second) {
1233
+ return JSON.stringify(this.serializeHostedChatView(first)) === JSON.stringify(this.serializeHostedChatView(second));
1234
+ }
1235
+ /** Persists presentation state without blocking customer-facing SDK methods. */
1236
+ persistPresentationState(options) {
1237
+ const cache = this.getPresentationCache();
1238
+ if (!cache) return;
1239
+ const state = {
1240
+ presented: options?.presented ?? this.presented,
1241
+ hostedChatView: this.serializeHostedChatView(this.hostedChatView),
1242
+ updatedAt: Date.now(),
1243
+ cleanShutdown: options?.cleanShutdown
1244
+ };
1245
+ void cache.set(PRESENTATION_CACHE_KEY, state).catch(() => void 0);
1246
+ }
1247
+ /** Restores Android presentation state after configure without resetting the hosted route to Home. */
1248
+ async restorePresentationFromCache() {
1249
+ const cache = this.getPresentationCache();
1250
+ if (!cache || Platform.OS !== "android") return;
1251
+ const persisted = await cache.get(PRESENTATION_CACHE_KEY).catch(() => null);
1252
+ if (persisted?.cleanShutdown) return;
1253
+ if (!isFreshPresentationState(persisted?.updatedAt)) return;
1254
+ if (!persisted?.presented) return;
1255
+ const hostedChatView = this.readPersistedHostedChatView(persisted.hostedChatView);
1256
+ if (!hostedChatView) return;
1257
+ this.hostedChatView = hostedChatView;
1258
+ this.presented = true;
1259
+ this.setUnreadCount(0);
1260
+ }
1108
1261
  };
1109
1262
  var internalSagepilotChat = new SagepilotReactNativeChat();
1110
1263
  var SagepilotChat = {
@@ -1141,34 +1294,6 @@ var SagepilotChat = {
1141
1294
  destroy: () => internalSagepilotChat.destroy()
1142
1295
  };
1143
1296
 
1144
- // src/core/storage/cache.ts
1145
- function createAsyncStorageCacheStorage(asyncStorage) {
1146
- return {
1147
- getItem: (key) => asyncStorage.getItem(key),
1148
- setItem: (key, value) => asyncStorage.setItem(key, value),
1149
- removeItem: (key) => asyncStorage.removeItem(key)
1150
- };
1151
- }
1152
- function createJsonCache(storage, namespace) {
1153
- return {
1154
- async get(key) {
1155
- const value = await storage.getItem(`${namespace}:${key}`);
1156
- if (!value) return null;
1157
- try {
1158
- return JSON.parse(value);
1159
- } catch {
1160
- return null;
1161
- }
1162
- },
1163
- async set(key, value) {
1164
- await storage.setItem(`${namespace}:${key}`, JSON.stringify(value));
1165
- },
1166
- async remove(key) {
1167
- await storage.removeItem(`${namespace}:${key}`);
1168
- }
1169
- };
1170
- }
1171
-
1172
1297
  // src/core/storage/fileStore.ts
1173
1298
  var DEFAULT_DIRECTORY_NAME = "sagepilot-attachments";
1174
1299
  function sanitizeKey(key) {
@@ -1226,21 +1351,30 @@ function createSagepilotFileStore(blobUtil, options = {}) {
1226
1351
  }
1227
1352
 
1228
1353
  // src/core/native/filePicker.ts
1229
- import { PermissionsAndroid, Platform } from "react-native";
1230
- var DEFAULT_IMAGE_MAX_DIMENSION = 1920;
1231
- var DEFAULT_IMAGE_QUALITY = 0.8;
1232
- var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
1233
- var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
1234
- var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
1235
- var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1236
- var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
1354
+ import { Alert, Linking, PermissionsAndroid, Platform as Platform2 } from "react-native";
1355
+
1356
+ // src/core/native/filePickerShared.ts
1237
1357
  var SagepilotFilePickerError = class extends Error {
1358
+ /** Creates a typed picker error that can be surfaced over the widget bridge. */
1238
1359
  constructor(code, message) {
1239
1360
  super(message);
1240
1361
  this.name = "SagepilotFilePickerError";
1241
1362
  this.code = code;
1242
1363
  }
1243
1364
  };
1365
+ var SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION = 1920;
1366
+ var SAGEPILOT_DEFAULT_IMAGE_QUALITY = 0.8;
1367
+ var SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
1368
+
1369
+ // src/core/native/filePicker.ts
1370
+ var DEBUG_PREFIX = "[SagepilotSDK][FilePicker]";
1371
+ var DEFAULT_IMAGE_MAX_DIMENSION = SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION;
1372
+ var DEFAULT_IMAGE_QUALITY = SAGEPILOT_DEFAULT_IMAGE_QUALITY;
1373
+ var DEFAULT_IMAGE_MIME_TYPE = "image/jpeg";
1374
+ var DEFAULT_DOCUMENT_MIME_TYPE = "application/octet-stream";
1375
+ var DEFAULT_IMAGE_SELECTION_LIMIT = 5;
1376
+ var DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1377
+ var DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES = SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1244
1378
  function bytesToMb(bytes) {
1245
1379
  return Math.round(bytes / (1024 * 1024) * 10) / 10;
1246
1380
  }
@@ -1250,15 +1384,44 @@ function estimateBase64ByteSize(dataBase64) {
1250
1384
  function stripFileScheme(uri) {
1251
1385
  return uri.startsWith("file://") ? uri.replace("file://", "") : uri;
1252
1386
  }
1253
- async function ensureAndroidCameraPermission() {
1254
- if (Platform.OS !== "android") return;
1387
+ function debugFilePicker(message, details) {
1388
+ console.log(`${DEBUG_PREFIX} ${message}`, details ?? "");
1389
+ }
1390
+ async function promptOpenSagepilotCameraSettings() {
1391
+ if (Platform2.OS !== "android") return;
1392
+ await new Promise((resolve) => {
1393
+ Alert.alert(
1394
+ "Camera access is off",
1395
+ "Enable camera access for this app in Settings to take a photo.",
1396
+ [
1397
+ {
1398
+ text: "Not now",
1399
+ style: "cancel",
1400
+ onPress: resolve
1401
+ },
1402
+ {
1403
+ text: "Open Settings",
1404
+ onPress: () => {
1405
+ Linking.openSettings().finally(resolve);
1406
+ }
1407
+ }
1408
+ ]
1409
+ );
1410
+ });
1411
+ }
1412
+ async function ensureSagepilotAndroidCameraPermission() {
1413
+ if (Platform2.OS !== "android") return;
1255
1414
  let result;
1256
1415
  try {
1416
+ debugFilePicker("requesting Android camera permission");
1257
1417
  result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
1258
1418
  } catch {
1419
+ debugFilePicker("camera permission request threw; continuing to native launch");
1259
1420
  return;
1260
1421
  }
1422
+ debugFilePicker("Android camera permission result", { result });
1261
1423
  if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
1424
+ await promptOpenSagepilotCameraSettings();
1262
1425
  throw new SagepilotFilePickerError(
1263
1426
  "permission_denied",
1264
1427
  "Camera access is turned off. Enable it for this app in Settings to take a photo."
@@ -1271,6 +1434,9 @@ async function ensureAndroidCameraPermission() {
1271
1434
  );
1272
1435
  }
1273
1436
  }
1437
+ function canUseCameraSource(imagePicker) {
1438
+ return Boolean(imagePicker);
1439
+ }
1274
1440
  function mapImagePickerErrorCode(errorCode) {
1275
1441
  switch (errorCode) {
1276
1442
  case "permission":
@@ -1347,8 +1513,16 @@ function createSagepilotFilePicker(options) {
1347
1513
  const documentMaxFileSizeBytes = options.documentMaxFileSizeBytes ?? DEFAULT_DOCUMENT_MAX_FILE_SIZE_BYTES;
1348
1514
  const imageMaxFileSizeBytes = options.imageMaxFileSizeBytes ?? DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES;
1349
1515
  const sources = [];
1350
- if (imagePicker) sources.push("camera", "library");
1516
+ if (canUseCameraSource(imagePicker)) sources.push("camera");
1517
+ if (imagePicker) sources.push("library");
1351
1518
  if (documentsPicker) sources.push("documents");
1519
+ debugFilePicker("adapter created", {
1520
+ platform: Platform2.OS,
1521
+ hasImagePicker: Boolean(imagePicker),
1522
+ hasDocumentsPicker: Boolean(documentsPicker),
1523
+ hasFileReader: Boolean(fileReader),
1524
+ sources
1525
+ });
1352
1526
  if (sources.length === 0) return void 0;
1353
1527
  const imageOptions = {
1354
1528
  mediaType: "photo",
@@ -1359,12 +1533,22 @@ function createSagepilotFilePicker(options) {
1359
1533
  saveToPhotos: false
1360
1534
  };
1361
1535
  async function pickFromCamera() {
1362
- if (!imagePicker) return [];
1363
- await ensureAndroidCameraPermission();
1536
+ debugFilePicker("camera pick requested", {
1537
+ platform: Platform2.OS,
1538
+ imageMaxDimension,
1539
+ imageQuality,
1540
+ imageMaxFileSizeBytes
1541
+ });
1542
+ await ensureSagepilotAndroidCameraPermission();
1543
+ if (!imagePicker) {
1544
+ throw new SagepilotFilePickerError("camera_unavailable", "The camera picker is not available in this build.");
1545
+ }
1546
+ debugFilePicker("launching image-picker camera", { platform: Platform2.OS });
1364
1547
  return imageAssetsToPickedFiles(await imagePicker.launchCamera(imageOptions), imageMaxFileSizeBytes);
1365
1548
  }
1366
1549
  async function pickFromLibrary(multiple) {
1367
1550
  if (!imagePicker) return [];
1551
+ debugFilePicker("library pick requested", { multiple, imageSelectionLimit });
1368
1552
  return imageAssetsToPickedFiles(await imagePicker.launchImageLibrary({
1369
1553
  ...imageOptions,
1370
1554
  // Bounded multi-select: 0 (unlimited) lets a huge batch OOM the bridge.
@@ -1373,6 +1557,7 @@ function createSagepilotFilePicker(options) {
1373
1557
  }
1374
1558
  async function pickDocuments(multiple) {
1375
1559
  if (!documentsPicker) return [];
1560
+ debugFilePicker("document pick requested", { multiple, documentMaxFileSizeBytes });
1376
1561
  let picked;
1377
1562
  try {
1378
1563
  picked = await documentsPicker.pick({ allowMultiSelection: multiple });
@@ -1422,6 +1607,7 @@ function createSagepilotFilePicker(options) {
1422
1607
  return {
1423
1608
  sources,
1424
1609
  async pickFiles(request) {
1610
+ debugFilePicker("pickFiles request", request);
1425
1611
  if (request.source === "camera") return pickFromCamera();
1426
1612
  if (request.source === "library") return pickFromLibrary(request.multiple);
1427
1613
  return pickDocuments(request.multiple);
@@ -1431,13 +1617,14 @@ function createSagepilotFilePicker(options) {
1431
1617
 
1432
1618
  // src/ui/SagepilotChatProvider.ts
1433
1619
  import { createElement, useCallback, useEffect, useRef, useState } from "react";
1620
+ import { Camera, FileText, Images, X } from "lucide-react-native";
1434
1621
  import {
1435
1622
  ActivityIndicator,
1436
1623
  AppState,
1437
1624
  KeyboardAvoidingView,
1438
- Linking,
1625
+ Linking as Linking2,
1439
1626
  Modal,
1440
- Platform as Platform2,
1627
+ Platform as Platform3,
1441
1628
  Pressable,
1442
1629
  SafeAreaView,
1443
1630
  StyleSheet,
@@ -1448,8 +1635,11 @@ import { WebView } from "react-native-webview";
1448
1635
 
1449
1636
  // src/core/webview/mobileBridge.ts
1450
1637
  var FILE_PICKER_PROTOCOL_VERSION = 2;
1638
+ function buildBridgeCapabilitiesPayload(capabilities) {
1639
+ return { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1640
+ }
1451
1641
  function buildBridgeCapabilitiesScript(capabilities) {
1452
- const payload = { ...capabilities, filePickerProtocol: FILE_PICKER_PROTOCOL_VERSION };
1642
+ const payload = buildBridgeCapabilitiesPayload(capabilities);
1453
1643
  return [
1454
1644
  "(function(){",
1455
1645
  "try {",
@@ -1498,9 +1688,23 @@ function parseHostedBridgeMessage(rawData) {
1498
1688
  return null;
1499
1689
  }
1500
1690
  }
1501
- var mobileWebViewBridgeScript = `
1691
+ function buildMobileWebViewBridgeScript(capabilities) {
1692
+ const payload = buildBridgeCapabilitiesPayload(capabilities);
1693
+ return `
1502
1694
  (function () {
1503
- if (window.__sagepilotRnBridgeInstalled) return true;
1695
+ var bridgeCapabilities = ${JSON.stringify(payload)};
1696
+ var applyBridgeCapabilities = function () {
1697
+ try {
1698
+ if (window.SagepilotMobileBridge) {
1699
+ window.SagepilotMobileBridge.capabilities = Object.assign({}, window.SagepilotMobileBridge.capabilities, bridgeCapabilities);
1700
+ }
1701
+ } catch (error) {}
1702
+ };
1703
+
1704
+ if (window.__sagepilotRnBridgeInstalled) {
1705
+ applyBridgeCapabilities();
1706
+ return true;
1707
+ }
1504
1708
  window.__sagepilotRnBridgeInstalled = true;
1505
1709
 
1506
1710
  var ensureViewport = function () {
@@ -1541,9 +1745,11 @@ var mobileWebViewBridgeScript = `
1541
1745
  };
1542
1746
 
1543
1747
  window.SagepilotMobileBridge = {
1748
+ capabilities: bridgeCapabilities,
1544
1749
  postMessage: sendToReactNative,
1545
1750
  ready: function () { sendToReactNative({ type: "sagepilot:ready" }); }
1546
1751
  };
1752
+ applyBridgeCapabilities();
1547
1753
 
1548
1754
  document.addEventListener("click", function (event) {
1549
1755
  try {
@@ -1584,6 +1790,7 @@ var mobileWebViewBridgeScript = `
1584
1790
  return true;
1585
1791
  })();
1586
1792
  `;
1793
+ }
1587
1794
 
1588
1795
  // src/specs/SagepilotInsetsViewNativeComponent.ts
1589
1796
  import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent";
@@ -1600,13 +1807,17 @@ function readPresentationState() {
1600
1807
  presentation: internalSagepilotChat.getConfig()?.presentation
1601
1808
  };
1602
1809
  }
1810
+ function getBridgeCapabilities() {
1811
+ return {
1812
+ nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1813
+ };
1814
+ }
1603
1815
  function getInjectedWebViewScript() {
1816
+ const capabilities = getBridgeCapabilities();
1604
1817
  return [
1605
1818
  internalSagepilotChat.getHostedAuthScript(),
1606
- mobileWebViewBridgeScript,
1607
- buildBridgeCapabilitiesScript({
1608
- nativeFilePicker: Boolean(internalSagepilotChat.getConfig()?.filePicker)
1609
- })
1819
+ buildMobileWebViewBridgeScript(capabilities),
1820
+ buildBridgeCapabilitiesScript(capabilities)
1610
1821
  ].filter(Boolean).join("\n");
1611
1822
  }
1612
1823
  var FILE_PICKER_SOURCE_LABELS = {
@@ -1614,6 +1825,22 @@ var FILE_PICKER_SOURCE_LABELS = {
1614
1825
  library: "Choose from gallery",
1615
1826
  documents: "Browse files"
1616
1827
  };
1828
+ var FILE_PICKER_SOURCE_DESCRIPTIONS = {
1829
+ camera: "Use the in-app camera",
1830
+ library: "Photos and videos on this device",
1831
+ documents: "PDFs, documents, and Drive files"
1832
+ };
1833
+ var FILE_PICKER_SOURCE_ICONS = {
1834
+ camera: Camera,
1835
+ library: Images,
1836
+ documents: FileText
1837
+ };
1838
+ var FILE_PICKER_SOURCE_ICON_COLORS = {
1839
+ camera: "#0284c7",
1840
+ library: "#16a34a",
1841
+ documents: "#7c3aed"
1842
+ };
1843
+ var DEBUG_PREFIX2 = "[SagepilotSDK][Provider]";
1617
1844
  var DELIVERY_RETRY_MS = 1500;
1618
1845
  var DELIVERY_LEGACY_FALLBACK_ATTEMPTS = 2;
1619
1846
  var DELIVERY_MAX_ATTEMPTS = 8;
@@ -1623,6 +1850,7 @@ var PENDING_FILES_CACHE_KEY = "pending_batch";
1623
1850
  var LIVENESS_HEARTBEAT_STALE_MS = 4e3;
1624
1851
  var LIVENESS_PONG_TIMEOUT_MS = 1500;
1625
1852
  var LIVENESS_MAX_PING_ATTEMPTS = 2;
1853
+ var LIVENESS_PICKER_GRACE_MS = 8e3;
1626
1854
  var LIVENESS_MAX_REMOUNTS = 2;
1627
1855
  var batchSequence = 0;
1628
1856
  function nextBatchId() {
@@ -1636,6 +1864,29 @@ function totalBase64Bytes(files) {
1636
1864
  function storageKeyFor(batchId, index) {
1637
1865
  return `${batchId}_${index}.bin`;
1638
1866
  }
1867
+ function renderFilePickerSourceIcon(source) {
1868
+ const Icon = FILE_PICKER_SOURCE_ICONS[source];
1869
+ return createElement(
1870
+ View,
1871
+ { style: getFilePickerSourceIconStyle(source) },
1872
+ createElement(Icon, {
1873
+ color: FILE_PICKER_SOURCE_ICON_COLORS[source],
1874
+ size: 23,
1875
+ strokeWidth: 2.35
1876
+ })
1877
+ );
1878
+ }
1879
+ function getFilePickerSourceIconStyle(source) {
1880
+ if (source === "camera") return [styles.sheetOptionIcon, styles.cameraOptionIcon];
1881
+ if (source === "library") return [styles.sheetOptionIcon, styles.libraryOptionIcon];
1882
+ return [styles.sheetOptionIcon, styles.documentsOptionIcon];
1883
+ }
1884
+ function getSheetOptionStyle(pressed) {
1885
+ return [styles.sheetOption, pressed && styles.sheetOptionPressed];
1886
+ }
1887
+ function getSheetCancelStyle(pressed) {
1888
+ return [styles.sheetCancelButton, pressed && styles.sheetCancelButtonPressed];
1889
+ }
1639
1890
  function buildDispatchScript(messageLiteral) {
1640
1891
  return [
1641
1892
  "(function(){",
@@ -1647,8 +1898,14 @@ function buildDispatchScript(messageLiteral) {
1647
1898
  "})();"
1648
1899
  ].join("\n");
1649
1900
  }
1650
- function buildFilesPickedScript(files, batchId) {
1651
- return buildDispatchScript(`{ type: "sagepilot:files_picked", batch_id: ${JSON.stringify(batchId)}, files: ${JSON.stringify(files)} }`);
1901
+ function buildFilesPickedScript(batch) {
1902
+ return buildDispatchScript(JSON.stringify({
1903
+ type: "sagepilot:files_picked",
1904
+ batch_id: batch.batchId,
1905
+ chat_id: batch.target?.chatId,
1906
+ conversation_id: batch.target?.chatId,
1907
+ files: batch.files
1908
+ }));
1652
1909
  }
1653
1910
  function buildFilePickerErrorScript(message, code) {
1654
1911
  return buildDispatchScript(
@@ -1667,6 +1924,16 @@ function readFilePickerError(error) {
1667
1924
  }
1668
1925
  return { message: "Could not attach the selected file." };
1669
1926
  }
1927
+ function debugProvider(message, details) {
1928
+ console.log(`${DEBUG_PREFIX2} ${message}`, details ?? "");
1929
+ }
1930
+ function describeAttachmentTarget(target) {
1931
+ return {
1932
+ targetChatId: target?.chatId,
1933
+ targetRouteChatId: target?.routeChatId,
1934
+ targetScreen: target?.hostedChatView.screen
1935
+ };
1936
+ }
1670
1937
  function getHostedIdentityDispatchScript() {
1671
1938
  const message = internalSagepilotChat.getHostedIdentityMessage();
1672
1939
  if (!message) return "";
@@ -1695,7 +1962,7 @@ function isInternalWebViewScheme(url) {
1695
1962
  var hostedChatWebViewProps = {
1696
1963
  allowFileAccess: true,
1697
1964
  allowFileAccessFromFileURLs: true,
1698
- ...Platform2.OS === "ios" ? {
1965
+ ...Platform3.OS === "ios" ? {
1699
1966
  automaticallyAdjustContentInsets: false,
1700
1967
  contentInsetAdjustmentBehavior: "never",
1701
1968
  hideKeyboardAccessoryView: true
@@ -1710,7 +1977,7 @@ var hostedChatWebViewProps = {
1710
1977
  sharedCookiesEnabled: true,
1711
1978
  thirdPartyCookiesEnabled: true
1712
1979
  };
1713
- var AndroidInsetsView = Platform2.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
1980
+ var AndroidInsetsView = Platform3.OS === "android" ? SagepilotInsetsViewNativeComponent_default : View;
1714
1981
  var emptyAndroidInsets = { top: 0, bottom: 0 };
1715
1982
  function SagepilotChatProvider({
1716
1983
  children,
@@ -1729,6 +1996,9 @@ function SagepilotChatProvider({
1729
1996
  const deliveryAttemptsRef = useRef(0);
1730
1997
  const pendingPingRef = useRef(null);
1731
1998
  const pingTimeoutRef = useRef(null);
1999
+ const livenessResumeTimerRef = useRef(null);
2000
+ const livenessPausedUntilRef = useRef(0);
2001
+ const pickerInFlightRef = useRef(false);
1732
2002
  const livenessRemountCountRef = useRef(0);
1733
2003
  const appStateRef = useRef(AppState.currentState);
1734
2004
  const didReconcileRef = useRef(false);
@@ -1751,6 +2021,7 @@ function SagepilotChatProvider({
1751
2021
  if (hasFileStore) {
1752
2022
  const manifest = queue.filter((batch) => batch.storageKeys && batch.storageKeys.length === batch.files.length).map((batch) => ({
1753
2023
  batchId: batch.batchId,
2024
+ target: batch.target,
1754
2025
  files: batch.files.map((file, index) => ({
1755
2026
  file_name: file.file_name,
1756
2027
  mime_type: file.mime_type,
@@ -1769,6 +2040,7 @@ function SagepilotChatProvider({
1769
2040
  if (totalBytes <= PERSIST_MAX_TOTAL_BYTES) {
1770
2041
  const manifest = queue.map((batch) => ({
1771
2042
  batchId: batch.batchId,
2043
+ target: batch.target,
1772
2044
  files: batch.files.map((file) => ({
1773
2045
  file_name: file.file_name,
1774
2046
  mime_type: file.mime_type,
@@ -1810,12 +2082,28 @@ function SagepilotChatProvider({
1810
2082
  }
1811
2083
  const batch = pendingBatchesRef.current[0];
1812
2084
  if (!batch) return;
2085
+ if (internalSagepilotChat.restoreHostedAttachmentTarget(batch.target)) {
2086
+ debugProvider("restored hosted attachment target", {
2087
+ batchId: batch.batchId,
2088
+ ...describeAttachmentTarget(batch.target)
2089
+ });
2090
+ widgetReadyRef.current = false;
2091
+ deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
2092
+ return;
2093
+ }
1813
2094
  deliveryAttemptsRef.current += 1;
1814
2095
  const attempts = deliveryAttemptsRef.current;
1815
2096
  const ref = nativeWebViewRef.current;
1816
2097
  const shouldDeliver = ref && (widgetReadyRef.current || attempts >= DELIVERY_LEGACY_FALLBACK_ATTEMPTS);
1817
2098
  if (shouldDeliver && ref) {
1818
- ref.injectJavaScript(buildFilesPickedScript(batch.files, batch.batchId));
2099
+ debugProvider("delivering native picked files", {
2100
+ batchId: batch.batchId,
2101
+ attempt: attempts,
2102
+ widgetReady: widgetReadyRef.current,
2103
+ count: batch.files.length,
2104
+ ...describeAttachmentTarget(batch.target)
2105
+ });
2106
+ ref.injectJavaScript(buildFilesPickedScript(batch));
1819
2107
  }
1820
2108
  if (attempts < DELIVERY_MAX_ATTEMPTS) {
1821
2109
  deliveryTimerRef.current = setTimeout(() => pumpDelivery(), DELIVERY_RETRY_MS);
@@ -1846,23 +2134,65 @@ function SagepilotChatProvider({
1846
2134
  const batchId = nextBatchId();
1847
2135
  const hasFileStore = Boolean(internalSagepilotChat.getConfig()?.fileStore);
1848
2136
  const storageKeys = hasFileStore ? files.map((_, index) => storageKeyFor(batchId, index)) : void 0;
1849
- const batch = { batchId, files, storageKeys };
2137
+ const batch = {
2138
+ batchId,
2139
+ files,
2140
+ storageKeys,
2141
+ target: internalSagepilotChat.getHostedAttachmentTarget()
2142
+ };
2143
+ debugProvider("queued native picked files", {
2144
+ batchId,
2145
+ count: files.length,
2146
+ totalBase64Bytes: totalBase64Bytes(files),
2147
+ persistedToFileStore: hasFileStore,
2148
+ ...describeAttachmentTarget(batch.target)
2149
+ });
1850
2150
  pendingBatchesRef.current = [...pendingBatchesRef.current, batch];
1851
2151
  ensureDelivery();
1852
2152
  writeManifest();
1853
2153
  void writeBatchBytes(batch);
1854
2154
  }, [ensureDelivery, writeManifest, writeBatchBytes]);
1855
- const recoverNativeWebView = useCallback(() => {
2155
+ const recoverNativeWebView = useCallback((reasonOrEvent) => {
2156
+ const reason = typeof reasonOrEvent === "string" ? reasonOrEvent : "webview_render_process";
2157
+ debugProvider("recovering native WebView", { reason });
1856
2158
  widgetReadyRef.current = false;
1857
2159
  setNativeWebViewKey((key) => key + 1);
1858
2160
  }, []);
1859
2161
  const recoverPreloadWebView = useCallback(() => {
1860
2162
  setPreloadWebViewKey((key) => key + 1);
1861
2163
  }, []);
2164
+ const pauseLivenessAfterPicker = useCallback((durationMs) => {
2165
+ if (Platform3.OS !== "android") return;
2166
+ livenessPausedUntilRef.current = Math.max(livenessPausedUntilRef.current, Date.now() + durationMs);
2167
+ }, []);
1862
2168
  const runNativeFilePicker = useCallback((source, multiple) => {
1863
2169
  const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1864
- if (!filePicker) return;
2170
+ if (!filePicker) {
2171
+ debugProvider("native file picker requested but no adapter is configured", { source, multiple });
2172
+ return;
2173
+ }
2174
+ if (pickerInFlightRef.current) {
2175
+ debugProvider("native file picker ignored because another picker is in flight", { source, multiple });
2176
+ return;
2177
+ }
2178
+ debugProvider("native file picker starting", {
2179
+ source,
2180
+ multiple,
2181
+ configuredSources: filePicker.sources
2182
+ });
2183
+ pauseLivenessAfterPicker(6e4);
2184
+ pickerInFlightRef.current = true;
1865
2185
  filePicker.pickFiles({ source, multiple }).then((files) => {
2186
+ debugProvider("native file picker resolved", {
2187
+ source,
2188
+ count: files.length,
2189
+ files: files.map((file) => ({
2190
+ fileName: file.file_name,
2191
+ mimeType: file.mime_type,
2192
+ size: file.size,
2193
+ base64Length: file.data_base64.length
2194
+ }))
2195
+ });
1866
2196
  if (files.length === 0) {
1867
2197
  nativeWebViewRef.current?.injectJavaScript(buildFilePickerCancelledScript());
1868
2198
  return;
@@ -1870,14 +2200,26 @@ function SagepilotChatProvider({
1870
2200
  queuePickedFiles(files);
1871
2201
  }).catch((error) => {
1872
2202
  const { message, code } = readFilePickerError(error);
2203
+ debugProvider("native file picker failed", { source, code, message });
1873
2204
  nativeWebViewRef.current?.injectJavaScript(buildFilePickerErrorScript(message, code));
2205
+ }).finally(() => {
2206
+ pickerInFlightRef.current = false;
2207
+ pauseLivenessAfterPicker(LIVENESS_PICKER_GRACE_MS);
1874
2208
  });
1875
- }, [queuePickedFiles]);
2209
+ }, [pauseLivenessAfterPicker, queuePickedFiles]);
1876
2210
  const openNativeFilePicker = useCallback((multiple) => {
1877
2211
  const filePicker = internalSagepilotChat.getConfig()?.filePicker;
1878
- if (!filePicker || filePicker.sources.length === 0) return;
2212
+ if (!filePicker || filePicker.sources.length === 0) {
2213
+ debugProvider("open native file picker ignored", {
2214
+ multiple,
2215
+ hasAdapter: Boolean(filePicker),
2216
+ sources: filePicker?.sources ?? []
2217
+ });
2218
+ return;
2219
+ }
2220
+ debugProvider("open native file picker", { multiple, sources: filePicker.sources });
1879
2221
  const [onlySource] = filePicker.sources;
1880
- if (filePicker.sources.length === 1 && onlySource) {
2222
+ if (filePicker.sources.length === 1 && onlySource && onlySource !== "camera") {
1881
2223
  runNativeFilePicker(onlySource, multiple);
1882
2224
  return;
1883
2225
  }
@@ -1930,7 +2272,8 @@ function SagepilotChatProvider({
1930
2272
  restored.push({
1931
2273
  batchId: batch.batchId,
1932
2274
  files,
1933
- storageKeys: storageKeys.length === files.length ? storageKeys : void 0
2275
+ storageKeys: storageKeys.length === files.length ? storageKeys : void 0,
2276
+ target: batch.target
1934
2277
  });
1935
2278
  }
1936
2279
  }
@@ -1951,6 +2294,19 @@ function SagepilotChatProvider({
1951
2294
  cancelled = true;
1952
2295
  };
1953
2296
  }, [getPendingCache, startDelivery, writeManifest]);
2297
+ const clearPing = useCallback(() => {
2298
+ pendingPingRef.current = null;
2299
+ if (pingTimeoutRef.current) {
2300
+ clearTimeout(pingTimeoutRef.current);
2301
+ pingTimeoutRef.current = null;
2302
+ }
2303
+ }, []);
2304
+ const clearDeferredLivenessProbe = useCallback(() => {
2305
+ if (livenessResumeTimerRef.current) {
2306
+ clearTimeout(livenessResumeTimerRef.current);
2307
+ livenessResumeTimerRef.current = null;
2308
+ }
2309
+ }, []);
1954
2310
  useEffect(() => {
1955
2311
  return () => {
1956
2312
  if (deliveryTimerRef.current) {
@@ -1961,19 +2317,27 @@ function SagepilotChatProvider({
1961
2317
  clearTimeout(pingTimeoutRef.current);
1962
2318
  pingTimeoutRef.current = null;
1963
2319
  }
2320
+ clearDeferredLivenessProbe();
1964
2321
  };
1965
- }, []);
1966
- const clearPing = useCallback(() => {
1967
- pendingPingRef.current = null;
1968
- if (pingTimeoutRef.current) {
1969
- clearTimeout(pingTimeoutRef.current);
1970
- pingTimeoutRef.current = null;
1971
- }
1972
- }, []);
2322
+ }, [clearDeferredLivenessProbe]);
1973
2323
  const runLivenessProbe = useCallback(() => {
1974
- if (Platform2.OS !== "android") return;
2324
+ if (Platform3.OS !== "android") return;
1975
2325
  const ref = nativeWebViewRef.current;
1976
2326
  if (!ref || !internalSagepilotChat.isPresented()) return;
2327
+ const pauseRemainingMs = livenessPausedUntilRef.current - Date.now();
2328
+ if (pickerInFlightRef.current || pauseRemainingMs > 0) {
2329
+ const retryDelayMs = pickerInFlightRef.current ? 500 : Math.max(0, pauseRemainingMs);
2330
+ debugProvider("liveness probe deferred after picker", {
2331
+ pickerInFlight: pickerInFlightRef.current,
2332
+ retryDelayMs
2333
+ });
2334
+ clearDeferredLivenessProbe();
2335
+ livenessResumeTimerRef.current = setTimeout(() => {
2336
+ livenessResumeTimerRef.current = null;
2337
+ runLivenessProbe();
2338
+ }, retryDelayMs);
2339
+ return;
2340
+ }
1977
2341
  setAndroidRepaintTick((tick) => tick + 1);
1978
2342
  const attempts = (pendingPingRef.current?.attempts ?? 0) + 1;
1979
2343
  const nonce = `${Date.now().toString(36)}_${attempts}`;
@@ -1983,12 +2347,12 @@ function SagepilotChatProvider({
1983
2347
  pingTimeoutRef.current = setTimeout(() => {
1984
2348
  if (attempts >= LIVENESS_MAX_PING_ATTEMPTS) {
1985
2349
  clearPing();
1986
- recoverNativeWebView();
2350
+ recoverNativeWebView("liveness_timeout");
1987
2351
  return;
1988
2352
  }
1989
2353
  runLivenessProbe();
1990
2354
  }, LIVENESS_PONG_TIMEOUT_MS);
1991
- }, [clearPing, recoverNativeWebView]);
2355
+ }, [clearDeferredLivenessProbe, clearPing, recoverNativeWebView]);
1992
2356
  useEffect(() => {
1993
2357
  const subscription = AppState.addEventListener("change", (nextState) => {
1994
2358
  const prev = appStateRef.current;
@@ -2018,11 +2382,11 @@ function SagepilotChatProvider({
2018
2382
  const presentationStyle = state.presentation?.style ?? "sheet";
2019
2383
  const isFullScreenModal = presentationStyle === "fullScreen";
2020
2384
  const animationType = presentationStyle === "fullScreen" || presentationStyle === "push" ? "slide" : "fade";
2021
- const ModalContainer = Platform2.OS === "ios" && isFullScreenModal ? SafeAreaView : View;
2022
- const NativeModalContainer = Platform2.OS === "android" ? AndroidInsetsView : ModalContainer;
2023
- const ChatContentContainer = Platform2.OS === "ios" ? KeyboardAvoidingView : View;
2024
- const nativeModalContainerProps = Platform2.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
2025
- const chatContentContainerProps = Platform2.OS === "ios" ? {
2385
+ const ModalContainer = Platform3.OS === "ios" && isFullScreenModal ? SafeAreaView : View;
2386
+ const NativeModalContainer = Platform3.OS === "android" ? AndroidInsetsView : ModalContainer;
2387
+ const ChatContentContainer = Platform3.OS === "ios" ? KeyboardAvoidingView : View;
2388
+ const nativeModalContainerProps = Platform3.OS === "android" ? { style: styles.container, onInsetsChange: handleAndroidInsetsChange } : { style: styles.container };
2389
+ const chatContentContainerProps = Platform3.OS === "ios" ? {
2026
2390
  behavior: "padding",
2027
2391
  enabled: true,
2028
2392
  keyboardVerticalOffset: 0,
@@ -2030,7 +2394,7 @@ function SagepilotChatProvider({
2030
2394
  } : {
2031
2395
  style: [
2032
2396
  styles.modalContent,
2033
- Platform2.OS === "android" ? {
2397
+ Platform3.OS === "android" ? {
2034
2398
  paddingTop: androidModalInsets.top,
2035
2399
  paddingBottom: androidModalInsets.bottom
2036
2400
  } : null
@@ -2051,6 +2415,11 @@ function SagepilotChatProvider({
2051
2415
  const ackBatchId = message.batch_id;
2052
2416
  const head = pendingBatchesRef.current[0];
2053
2417
  if (head && (!ackBatchId || ackBatchId === head.batchId)) {
2418
+ debugProvider("native picked files acknowledged", {
2419
+ batchId: head.batchId,
2420
+ ackBatchId,
2421
+ count: message.count
2422
+ });
2054
2423
  acknowledgeHeadBatch();
2055
2424
  }
2056
2425
  return;
@@ -2060,9 +2429,13 @@ function SagepilotChatProvider({
2060
2429
  if (pending && (!message.nonce || message.nonce === pending.nonce)) {
2061
2430
  if (message.alive === false) {
2062
2431
  clearPing();
2432
+ debugProvider("liveness probe reported dead widget", {
2433
+ supportsHeartbeat: message.supportsHeartbeat,
2434
+ remountCount: livenessRemountCountRef.current
2435
+ });
2063
2436
  if (livenessRemountCountRef.current < LIVENESS_MAX_REMOUNTS) {
2064
2437
  livenessRemountCountRef.current += 1;
2065
- recoverNativeWebView();
2438
+ recoverNativeWebView("liveness_dead_pong");
2066
2439
  }
2067
2440
  } else {
2068
2441
  livenessRemountCountRef.current = 0;
@@ -2084,7 +2457,13 @@ function SagepilotChatProvider({
2084
2457
  if (!script || !nativeWebViewRef.current) return;
2085
2458
  nativeWebViewRef.current.injectJavaScript(script);
2086
2459
  };
2460
+ const injectNativeBridgeToNativeWebView = () => {
2461
+ if (!nativeWebViewRef.current) return;
2462
+ nativeWebViewRef.current.injectJavaScript(getInjectedWebViewScript());
2463
+ };
2087
2464
  const handleNativeWebViewLoadEnd = () => {
2465
+ debugProvider("native WebView load end");
2466
+ injectNativeBridgeToNativeWebView();
2088
2467
  postIdentityToNativeWebView();
2089
2468
  widgetReadyRef.current = false;
2090
2469
  startDelivery();
@@ -2098,11 +2477,11 @@ function SagepilotChatProvider({
2098
2477
  if (widgetOrigin && readUrlOrigin(url) === widgetOrigin) {
2099
2478
  return true;
2100
2479
  }
2101
- Linking.openURL(url).catch(() => void 0);
2480
+ Linking2.openURL(url).catch(() => void 0);
2102
2481
  return false;
2103
2482
  }, [state.conversationUrl, state.preloadUrl]);
2104
2483
  useEffect(() => {
2105
- if (Platform2.OS !== "web" || typeof window === "undefined") return;
2484
+ if (Platform3.OS !== "web" || typeof window === "undefined") return;
2106
2485
  const trustedWidgetOrigin = readUrlOrigin(state.conversationUrl);
2107
2486
  if (!trustedWidgetOrigin) return;
2108
2487
  const handleWindowMessage = (event) => {
@@ -2113,14 +2492,14 @@ function SagepilotChatProvider({
2113
2492
  return () => window.removeEventListener("message", handleWindowMessage);
2114
2493
  }, [state.conversationUrl]);
2115
2494
  useEffect(() => {
2116
- if (Platform2.OS !== "web" || !state.isPresented) return;
2495
+ if (Platform3.OS !== "web" || !state.isPresented) return;
2117
2496
  postIdentityToWebFrame();
2118
2497
  }, [state.isPresented, state.conversationUrl, state.configured]);
2119
2498
  useEffect(() => {
2120
- if (Platform2.OS === "web" || !state.isPresented) return;
2499
+ if (Platform3.OS === "web" || !state.isPresented) return;
2121
2500
  postIdentityToNativeWebView();
2122
2501
  }, [state.isPresented, state.conversationUrl, state.configured]);
2123
- if (Platform2.OS === "web") {
2502
+ if (Platform3.OS === "web") {
2124
2503
  return createElement(
2125
2504
  View,
2126
2505
  { style: styles.root },
@@ -2182,8 +2561,8 @@ function SagepilotChatProvider({
2182
2561
  visible: state.configured && state.isPresented,
2183
2562
  animationType,
2184
2563
  presentationStyle: isFullScreenModal ? "fullScreen" : "pageSheet",
2185
- statusBarTranslucent: Platform2.OS === "android",
2186
- navigationBarTranslucent: Platform2.OS === "android",
2564
+ statusBarTranslucent: Platform3.OS === "android",
2565
+ navigationBarTranslucent: Platform3.OS === "android",
2187
2566
  onRequestClose: () => internalSagepilotChat.dismiss()
2188
2567
  },
2189
2568
  createElement(
@@ -2214,7 +2593,7 @@ function SagepilotChatProvider({
2214
2593
  // The imperceptible opacity toggle forces the Android WebView
2215
2594
  // surface to re-composite on resume, clearing the blank-but-alive
2216
2595
  // surface bug without a reload (see runLivenessProbe).
2217
- style: Platform2.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
2596
+ style: Platform3.OS === "android" ? [styles.webview, { opacity: 1 - androidRepaintTick % 2 * 1e-3 }] : styles.webview,
2218
2597
  startInLoadingState: true,
2219
2598
  injectedJavaScriptBeforeContentLoaded: getInjectedWebViewScript(),
2220
2599
  onMessage: handleWebViewMessage,
@@ -2252,6 +2631,7 @@ function SagepilotChatProvider({
2252
2631
  createElement(
2253
2632
  View,
2254
2633
  { style: styles.sheetCard },
2634
+ createElement(View, { style: styles.sheetHandle }),
2255
2635
  createElement(Text, { style: styles.sheetTitle }, "Add attachment"),
2256
2636
  ...(internalSagepilotChat.getConfig()?.filePicker?.sources ?? []).map(
2257
2637
  (source) => createElement(
@@ -2259,19 +2639,29 @@ function SagepilotChatProvider({
2259
2639
  {
2260
2640
  key: source,
2261
2641
  accessibilityRole: "button",
2262
- style: styles.sheetButton,
2642
+ android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
2643
+ hitSlop: 4,
2644
+ style: ({ pressed }) => getSheetOptionStyle(pressed),
2263
2645
  onPress: () => handleSourceChoice(source)
2264
2646
  },
2265
- createElement(Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source])
2647
+ renderFilePickerSourceIcon(source),
2648
+ createElement(
2649
+ View,
2650
+ { style: styles.sheetOptionText },
2651
+ createElement(Text, { style: styles.sheetButtonText }, FILE_PICKER_SOURCE_LABELS[source]),
2652
+ createElement(Text, { style: styles.sheetButtonDescription }, FILE_PICKER_SOURCE_DESCRIPTIONS[source])
2653
+ )
2266
2654
  )
2267
2655
  ),
2268
2656
  createElement(
2269
2657
  Pressable,
2270
2658
  {
2271
2659
  accessibilityRole: "button",
2272
- style: [styles.sheetButton, styles.sheetCancelButton],
2660
+ android_ripple: { color: "rgba(15, 23, 42, 0.06)" },
2661
+ style: ({ pressed }) => getSheetCancelStyle(pressed),
2273
2662
  onPress: () => setSourceChooser(null)
2274
2663
  },
2664
+ createElement(X, { color: "#334155", size: 18, strokeWidth: 2.5, style: styles.sheetCancelIcon }),
2275
2665
  createElement(Text, { style: styles.sheetCancelText }, "Cancel")
2276
2666
  )
2277
2667
  )
@@ -2377,43 +2767,109 @@ var styles = StyleSheet.create({
2377
2767
  },
2378
2768
  sheetBackdrop: {
2379
2769
  flex: 1,
2770
+ alignItems: "center",
2380
2771
  justifyContent: "flex-end",
2381
- backgroundColor: "rgba(17, 24, 39, 0.36)"
2772
+ backgroundColor: "rgba(15, 23, 42, 0.48)"
2382
2773
  },
2383
2774
  sheetCard: {
2775
+ width: "100%",
2776
+ maxWidth: 540,
2384
2777
  backgroundColor: "#ffffff",
2385
- borderTopLeftRadius: 16,
2386
- borderTopRightRadius: 16,
2387
- paddingTop: 8,
2388
- paddingBottom: 24,
2389
- paddingHorizontal: 8
2778
+ borderTopLeftRadius: 26,
2779
+ borderTopRightRadius: 26,
2780
+ paddingTop: 10,
2781
+ paddingBottom: 18,
2782
+ paddingHorizontal: 14,
2783
+ shadowColor: "#0f172a",
2784
+ shadowOpacity: 0.22,
2785
+ shadowRadius: 28,
2786
+ shadowOffset: { width: 0, height: -10 },
2787
+ elevation: 18
2788
+ },
2789
+ sheetHandle: {
2790
+ alignSelf: "center",
2791
+ width: 42,
2792
+ height: 5,
2793
+ borderRadius: 999,
2794
+ backgroundColor: "#d1d5db",
2795
+ marginBottom: 14
2390
2796
  },
2391
2797
  sheetTitle: {
2392
- textAlign: "center",
2393
- color: "#6b7280",
2394
- fontSize: 13,
2395
- fontWeight: "600",
2396
- paddingVertical: 10
2798
+ color: "#111827",
2799
+ fontSize: 17,
2800
+ fontWeight: "700",
2801
+ paddingBottom: 12,
2802
+ paddingHorizontal: 4
2397
2803
  },
2398
- sheetButton: {
2399
- minHeight: 52,
2804
+ sheetOption: {
2805
+ minHeight: 70,
2806
+ flexDirection: "row",
2807
+ alignItems: "center",
2808
+ borderRadius: 18,
2809
+ paddingHorizontal: 12,
2810
+ marginBottom: 6,
2811
+ backgroundColor: "#ffffff"
2812
+ },
2813
+ sheetOptionPressed: {
2814
+ backgroundColor: "#f8fafc",
2815
+ transform: [{ scale: 0.985 }]
2816
+ },
2817
+ sheetOptionIcon: {
2818
+ width: 46,
2819
+ height: 46,
2820
+ borderRadius: 16,
2821
+ marginRight: 14,
2400
2822
  alignItems: "center",
2401
2823
  justifyContent: "center",
2402
- borderRadius: 12
2824
+ position: "relative",
2825
+ overflow: "hidden"
2826
+ },
2827
+ sheetOptionText: {
2828
+ flex: 1,
2829
+ justifyContent: "center"
2403
2830
  },
2404
2831
  sheetButtonText: {
2405
- color: "#111827",
2832
+ color: "#0f172a",
2406
2833
  fontSize: 16,
2407
- fontWeight: "500"
2834
+ fontWeight: "700"
2835
+ },
2836
+ sheetButtonDescription: {
2837
+ color: "#64748b",
2838
+ fontSize: 13,
2839
+ fontWeight: "500",
2840
+ marginTop: 3
2841
+ },
2842
+ cameraOptionIcon: {
2843
+ backgroundColor: "#e0f2fe"
2844
+ },
2845
+ libraryOptionIcon: {
2846
+ backgroundColor: "#ecfdf5"
2847
+ },
2848
+ documentsOptionIcon: {
2849
+ backgroundColor: "#f5f3ff"
2408
2850
  },
2409
2851
  sheetCancelButton: {
2410
- marginTop: 6,
2411
- backgroundColor: "#f3f4f6"
2852
+ minHeight: 56,
2853
+ flexDirection: "row",
2854
+ alignItems: "center",
2855
+ justifyContent: "center",
2856
+ borderRadius: 18,
2857
+ marginTop: 8,
2858
+ backgroundColor: "#f1f5f9"
2859
+ },
2860
+ sheetCancelButtonPressed: {
2861
+ backgroundColor: "#e2e8f0",
2862
+ transform: [{ scale: 0.985 }]
2863
+ },
2864
+ sheetCancelIcon: {
2865
+ width: 18,
2866
+ height: 18,
2867
+ marginRight: 8
2412
2868
  },
2413
2869
  sheetCancelText: {
2414
- color: "#111827",
2870
+ color: "#0f172a",
2415
2871
  fontSize: 16,
2416
- fontWeight: "600"
2872
+ fontWeight: "700"
2417
2873
  }
2418
2874
  });
2419
2875
 
@@ -2467,6 +2923,9 @@ function useSagepilotChat() {
2467
2923
  };
2468
2924
  }
2469
2925
  export {
2926
+ SAGEPILOT_DEFAULT_IMAGE_MAX_DIMENSION,
2927
+ SAGEPILOT_DEFAULT_IMAGE_MAX_FILE_SIZE_BYTES,
2928
+ SAGEPILOT_DEFAULT_IMAGE_QUALITY,
2470
2929
  SagepilotChat,
2471
2930
  SagepilotChatError,
2472
2931
  SagepilotChatProvider,
@@ -2475,5 +2934,7 @@ export {
2475
2934
  createKeychainTokenStorage,
2476
2935
  createSagepilotFilePicker,
2477
2936
  createSagepilotFileStore,
2937
+ ensureSagepilotAndroidCameraPermission,
2938
+ promptOpenSagepilotCameraSettings,
2478
2939
  useSagepilotChat
2479
2940
  };