@paymanai/payman-ask-sdk 4.0.24 → 4.0.26

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.
@@ -55,6 +55,7 @@ var chatStore = {
55
55
  memoryStore.delete(key);
56
56
  }
57
57
  };
58
+ var EMPTY_USER_ACTION_STATE = { prompts: [], notifications: [] };
58
59
  var streams = /* @__PURE__ */ new Map();
59
60
  var activeStreamStore = {
60
61
  has(key) {
@@ -63,7 +64,11 @@ var activeStreamStore = {
63
64
  get(key) {
64
65
  const entry = streams.get(key);
65
66
  if (!entry) return null;
66
- return { messages: entry.messages, isWaiting: entry.isWaiting };
67
+ return {
68
+ messages: entry.messages,
69
+ isWaiting: entry.isWaiting,
70
+ userActionState: entry.userActionState
71
+ };
67
72
  },
68
73
  // Called before startStream — registers the controller and initial messages
69
74
  start(key, abortController, initialMessages) {
@@ -71,6 +76,7 @@ var activeStreamStore = {
71
76
  streams.set(key, {
72
77
  messages: initialMessages,
73
78
  isWaiting: true,
79
+ userActionState: EMPTY_USER_ACTION_STATE,
74
80
  abortController,
75
81
  listeners: existing?.listeners ?? /* @__PURE__ */ new Set()
76
82
  });
@@ -81,20 +87,27 @@ var activeStreamStore = {
81
87
  if (!entry) return;
82
88
  const next = typeof updater === "function" ? updater(entry.messages) : updater;
83
89
  entry.messages = next;
84
- entry.listeners.forEach((l) => l(next, entry.isWaiting));
90
+ entry.listeners.forEach((l) => l(next, entry.isWaiting, entry.userActionState));
85
91
  },
86
92
  setWaiting(key, waiting) {
87
93
  const entry = streams.get(key);
88
94
  if (!entry) return;
89
95
  entry.isWaiting = waiting;
90
- entry.listeners.forEach((l) => l(entry.messages, waiting));
96
+ entry.listeners.forEach((l) => l(entry.messages, waiting, entry.userActionState));
97
+ },
98
+ applyUserActionState(key, updater) {
99
+ const entry = streams.get(key);
100
+ if (!entry) return;
101
+ const next = typeof updater === "function" ? updater(entry.userActionState) : updater;
102
+ entry.userActionState = next;
103
+ entry.listeners.forEach((l) => l(entry.messages, entry.isWaiting, next));
91
104
  },
92
105
  // Called when stream completes — persists to chatStore and cleans up
93
106
  complete(key) {
94
107
  const entry = streams.get(key);
95
108
  if (!entry) return;
96
109
  entry.isWaiting = false;
97
- entry.listeners.forEach((l) => l(entry.messages, false));
110
+ entry.listeners.forEach((l) => l(entry.messages, false, entry.userActionState));
98
111
  const toSave = entry.messages.filter((m) => !m.isStreaming);
99
112
  if (toSave.length > 0) chatStore.set(key, toSave);
100
113
  streams.delete(key);
@@ -282,6 +295,8 @@ function getEventMessage(event) {
282
295
  }
283
296
  case "RUN_IN_PROGRESS":
284
297
  return event.partialText || "Thinking...";
298
+ case "LLM_CALL_STARTED":
299
+ return event.description || "";
285
300
  case "RUN_COMPLETED":
286
301
  return "Agent run completed";
287
302
  case "RUN_FAILED":
@@ -419,7 +434,8 @@ function createInitialV2State() {
419
434
  finalData: void 0,
420
435
  steps: [],
421
436
  stepCounter: 0,
422
- currentExecutingStepId: void 0
437
+ currentExecutingStepId: void 0,
438
+ trailingActivity: false
423
439
  };
424
440
  }
425
441
  function upsertUserAction(state, req) {
@@ -441,6 +457,7 @@ function processStreamEventV2(rawEvent, state) {
441
457
  if (typeof eventType === "string" && eventType.toUpperCase() === "KEEP_ALIVE") {
442
458
  if (event.executionId) state.executionId = event.executionId;
443
459
  if (event.sessionId) state.sessionId = event.sessionId;
460
+ if (state.finalResponse) state.trailingActivity = true;
444
461
  const description = typeof event.description === "string" ? event.description : "";
445
462
  if (description) {
446
463
  for (let i = state.steps.length - 1; i >= 0; i--) {
@@ -475,7 +492,12 @@ function processStreamEventV2(rawEvent, state) {
475
492
  state.lastEventType = eventType;
476
493
  break;
477
494
  }
495
+ case "LLM_CALL_STARTED":
496
+ if (state.finalResponse) state.trailingActivity = true;
497
+ state.lastEventType = eventType;
498
+ break;
478
499
  case "INTENT_STARTED": {
500
+ if (state.finalResponse) state.trailingActivity = true;
479
501
  const stepId = `step-${state.stepCounter++}`;
480
502
  state.steps.push({
481
503
  id: stepId,
@@ -491,6 +513,7 @@ function processStreamEventV2(rawEvent, state) {
491
513
  break;
492
514
  }
493
515
  case "INTENT_COMPLETED": {
516
+ if (state.finalResponse) state.trailingActivity = true;
494
517
  const intentStep = state.steps.find((s2) => s2.eventType === "INTENT_STARTED" && s2.status === "in_progress");
495
518
  if (intentStep) {
496
519
  intentStep.status = "completed";
@@ -522,6 +545,7 @@ function processStreamEventV2(rawEvent, state) {
522
545
  step2.status = "completed";
523
546
  }
524
547
  });
548
+ state.trailingActivity = false;
525
549
  state.lastEventType = eventType;
526
550
  break;
527
551
  }
@@ -593,6 +617,7 @@ function processStreamEventV2(rawEvent, state) {
593
617
  case "WORKFLOW_ERROR":
594
618
  state.hasError = true;
595
619
  state.errorMessage = event.errorMessage || event.message || "Workflow error";
620
+ state.trailingActivity = false;
596
621
  state.lastEventType = eventType;
597
622
  break;
598
623
  // ---- K2 pipeline stage lifecycle events ----
@@ -731,8 +756,7 @@ function buildRequestBody(config, userMessage, sessionId, options) {
731
756
  sessionAttributes,
732
757
  analysisMode: options?.analysisMode,
733
758
  locale: resolveLocale(config.locale),
734
- timezone: resolveTimezone(config.timezone),
735
- ...options?.attachments?.length ? { attachments: options.attachments } : {}
759
+ timezone: resolveTimezone(config.timezone)
736
760
  };
737
761
  }
738
762
  function resolveLocale(configured) {
@@ -777,7 +801,6 @@ function buildResolveImagesUrl(config) {
777
801
  const basePath = normalizedEndpointPath.endsWith("/stream") ? normalizedEndpointPath.slice(0, -"/stream".length) : normalizedEndpointPath;
778
802
  return `${config.api.baseUrl}${basePath}/resolve-image-urls`;
779
803
  }
780
- var NGROK_SKIP_BROWSER_WARNING = "ngrok-skip-browser-warning";
781
804
  function buildRequestHeaders(config) {
782
805
  const headers = {
783
806
  ...config.api.headers
@@ -785,9 +808,6 @@ function buildRequestHeaders(config) {
785
808
  if (config.api.authToken) {
786
809
  headers.Authorization = `Bearer ${config.api.authToken}`;
787
810
  }
788
- if (!headers[NGROK_SKIP_BROWSER_WARNING]) {
789
- headers[NGROK_SKIP_BROWSER_WARNING] = "true";
790
- }
791
811
  return headers;
792
812
  }
793
813
  var RAG_IMAGE_REGEX = /\/api\/rag\/chunks\/[^"'\s]+\/image/;
@@ -886,7 +906,9 @@ function useStreamManagerV2(config, callbacks, setMessages, setIsWaitingForRespo
886
906
  const latestUsefulStep = [...state.steps].reverse().find(
887
907
  (s2) => s2.message && !isBlandStatus(s2.message)
888
908
  );
889
- const currentMessage = useful(activeStep?.message) ?? useful(lastInProgressStep?.message) ?? latestUsefulStep?.message ?? useful(getEventMessage(event)) ?? // Last-resort: every candidate is bland (very first event,
909
+ const eventTypeUpper = typeof event.eventType === "string" ? event.eventType.toUpperCase() : "";
910
+ const liveEventLabel = eventTypeUpper === "KEEP_ALIVE" || eventTypeUpper === "LLM_CALL_STARTED" ? useful(getEventMessage(event)) : void 0;
911
+ const currentMessage = useful(activeStep?.message) ?? useful(lastInProgressStep?.message) ?? liveEventLabel ?? latestUsefulStep?.message ?? useful(getEventMessage(event)) ?? // Last-resort: every candidate is bland (very first event,
890
912
  // nothing useful seen yet). Render the bland label so the
891
913
  // bubble isn't blank.
892
914
  activeStep?.message ?? lastInProgressStep?.message ?? getEventMessage(event);
@@ -911,6 +933,7 @@ function useStreamManagerV2(config, callbacks, setMessages, setIsWaitingForRespo
911
933
  streamingContent: state.finalResponse,
912
934
  content: "",
913
935
  currentMessage,
936
+ hasTrailingActivity: state.trailingActivity,
914
937
  streamProgress: "processing",
915
938
  isError: false,
916
939
  steps: [...state.steps],
@@ -1068,81 +1091,6 @@ function createCancelledMessageUpdate(steps, currentMessage) {
1068
1091
  currentMessage: currentMessage || "Thinking..."
1069
1092
  };
1070
1093
  }
1071
- var DEFAULT_SIGNED_URL_ENDPOINT = "/api/files/signed-url";
1072
- function fileExtension(filename) {
1073
- const ext = filename.split(".").pop()?.toLowerCase().replace(/^\./, "");
1074
- return ext && ext.length > 0 ? ext : "bin";
1075
- }
1076
- function resolveMimeType(file) {
1077
- if (file.type && file.type.trim().length > 0) return file.type;
1078
- const ext = fileExtension(file.name);
1079
- const byExt = {
1080
- pdf: "application/pdf",
1081
- png: "image/png",
1082
- jpg: "image/jpeg",
1083
- jpeg: "image/jpeg",
1084
- gif: "image/gif",
1085
- webp: "image/webp"
1086
- };
1087
- return byExt[ext] ?? "application/octet-stream";
1088
- }
1089
- function buildSignedUrlEndpoint(config, ext) {
1090
- const endpoint = config.api.signedUrlEndpoint || DEFAULT_SIGNED_URL_ENDPOINT;
1091
- const queryParams = new URLSearchParams({ extn: ext.replace(/^\./, "") });
1092
- return `${config.api.baseUrl}${endpoint}?${queryParams.toString()}`;
1093
- }
1094
- async function requestSignedUrl(config, ext, signal) {
1095
- const url = buildSignedUrlEndpoint(config, ext);
1096
- const headers = buildRequestHeaders(config);
1097
- const response = await fetch(url, {
1098
- method: "GET",
1099
- headers,
1100
- signal
1101
- });
1102
- if (!response.ok) {
1103
- const errorText = await response.text().catch(() => "");
1104
- throw new Error(
1105
- errorText ? `Failed to get upload URL (${response.status}): ${errorText}` : `Failed to get upload URL (${response.status})`
1106
- );
1107
- }
1108
- const data = await response.json();
1109
- if (!data.key || !data.url) {
1110
- throw new Error("Signed URL response missing key or url");
1111
- }
1112
- return { key: data.key, url: data.url };
1113
- }
1114
- async function uploadToSignedUrl(signedUrl, file, mimeType, signal) {
1115
- const response = await fetch(signedUrl, {
1116
- method: "PUT",
1117
- headers: {
1118
- "Content-Type": mimeType,
1119
- "x-ms-blob-type": "BlockBlob"
1120
- },
1121
- body: file,
1122
- signal
1123
- });
1124
- if (!response.ok) {
1125
- const errorText = await response.text().catch(() => "");
1126
- throw new Error(
1127
- errorText ? `Failed to upload file (${response.status}): ${errorText}` : `Failed to upload file (${response.status})`
1128
- );
1129
- }
1130
- }
1131
- async function uploadAttachment(config, file, signal) {
1132
- const ext = fileExtension(file.name);
1133
- const mimeType = resolveMimeType(file);
1134
- const { key, url } = await requestSignedUrl(config, ext, signal);
1135
- await uploadToSignedUrl(url, file, mimeType, signal);
1136
- return {
1137
- tempKey: key,
1138
- filename: file.name,
1139
- mimeType
1140
- };
1141
- }
1142
- async function uploadAttachments(config, files, signal) {
1143
- const uploads = files.map((file) => uploadAttachment(config, file, signal));
1144
- return Promise.all(uploads);
1145
- }
1146
1094
  var UserActionStaleError = class extends Error {
1147
1095
  constructor(userActionId, message = "User action is no longer actionable") {
1148
1096
  super(message);
@@ -1179,12 +1127,38 @@ async function cancelUserAction(config, userActionId) {
1179
1127
  async function resendUserAction(config, userActionId) {
1180
1128
  return sendUserActionRequest(config, userActionId, "resend");
1181
1129
  }
1182
- var EMPTY_USER_ACTION_STATE = { prompts: [], notifications: [] };
1130
+ async function expireUserAction(config, userActionId) {
1131
+ return sendUserActionRequest(config, userActionId, "expired");
1132
+ }
1133
+ function resolveUserActionExpiresAt(req, existing) {
1134
+ if (typeof req.expirySeconds !== "number" || req.expirySeconds <= 0) {
1135
+ return void 0;
1136
+ }
1137
+ if (existing?.expiresAt && existing.userActionId === req.userActionId) {
1138
+ return existing.expiresAt;
1139
+ }
1140
+ return Date.now() + Math.floor(req.expirySeconds) * 1e3;
1141
+ }
1142
+ function getUserActionSecondsLeft(prompt) {
1143
+ if (typeof prompt.expiresAt === "number" && prompt.expiresAt > 0) {
1144
+ return Math.max(0, Math.ceil((prompt.expiresAt - Date.now()) / 1e3));
1145
+ }
1146
+ if (typeof prompt.expirySeconds === "number" && prompt.expirySeconds > 0) {
1147
+ return Math.floor(prompt.expirySeconds);
1148
+ }
1149
+ return void 0;
1150
+ }
1151
+ function isUserActionExpired(prompt) {
1152
+ const left = getUserActionSecondsLeft(prompt);
1153
+ return left !== void 0 && left <= 0;
1154
+ }
1155
+ var EMPTY_USER_ACTION_STATE2 = { prompts: [], notifications: [] };
1183
1156
  function upsertPrompt(prompts, req) {
1184
- const active = { ...req, status: "pending" };
1185
1157
  const idx = prompts.findIndex(
1186
1158
  (p) => req.toolCallId ? p.toolCallId === req.toolCallId : p.userActionId === req.userActionId
1187
1159
  );
1160
+ const expiresAt = resolveUserActionExpiresAt(req, idx >= 0 ? prompts[idx] : void 0);
1161
+ const active = { ...req, status: "pending", expiresAt };
1188
1162
  if (idx >= 0) {
1189
1163
  const next = prompts.slice();
1190
1164
  next[idx] = active;
@@ -1207,32 +1181,12 @@ function getStoredOrInitialMessages(config) {
1207
1181
  function getSessionIdFromMessages(messages) {
1208
1182
  return messages.find((message) => message.sessionId)?.sessionId;
1209
1183
  }
1210
- var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["jpg", "jpeg", "png", "gif", "webp", "avif"]);
1211
- function attachmentKindFromFile(file) {
1212
- const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
1213
- if (IMAGE_EXTENSIONS.has(ext) || file.type.startsWith("image/")) return "image";
1214
- return "file";
1215
- }
1216
- function buildMessageAttachments(files) {
1217
- return files.map((file, index) => {
1218
- const kind = attachmentKindFromFile(file);
1219
- return {
1220
- id: `att-${Date.now()}-${index}`,
1221
- filename: file.name,
1222
- mimeType: file.type || "application/octet-stream",
1223
- // Blob URL for all local files so PDFs/docs stay previewable after send.
1224
- previewUrl: URL.createObjectURL(file),
1225
- kind
1226
- };
1227
- });
1228
- }
1229
1184
  function useChatV2(config, callbacks = {}) {
1230
1185
  const [messages, setMessages] = react.useState(() => getStoredOrInitialMessages(config));
1231
1186
  const [isWaitingForResponse, setIsWaitingForResponse] = react.useState(() => {
1232
1187
  if (!config.userId) return false;
1233
1188
  return activeStreamStore.get(config.userId)?.isWaiting ?? false;
1234
1189
  });
1235
- const [isUploadingAttachments, setIsUploadingAttachments] = react.useState(false);
1236
1190
  const sessionIdRef = react.useRef(
1237
1191
  getSessionIdFromMessages(getStoredOrInitialMessages(config)) ?? config.initialSessionId ?? void 0
1238
1192
  );
@@ -1275,7 +1229,28 @@ function useChatV2(config, callbacks = {}) {
1275
1229
  // eslint-disable-next-line react-hooks/exhaustive-deps
1276
1230
  []
1277
1231
  );
1278
- const [userActionState, setUserActionState] = react.useState(EMPTY_USER_ACTION_STATE);
1232
+ const [userActionState, setUserActionState] = react.useState(() => {
1233
+ if (!config.userId) return EMPTY_USER_ACTION_STATE2;
1234
+ return activeStreamStore.get(config.userId)?.userActionState ?? EMPTY_USER_ACTION_STATE2;
1235
+ });
1236
+ const storeAwareSetUserActionState = react.useCallback(
1237
+ (updater) => {
1238
+ const streamUserId = streamUserIdRef.current;
1239
+ const currentUserId = configRef.current.userId;
1240
+ const storeKey = streamUserId ?? currentUserId;
1241
+ if (storeKey && activeStreamStore.has(storeKey)) {
1242
+ activeStreamStore.applyUserActionState(
1243
+ storeKey,
1244
+ updater
1245
+ );
1246
+ }
1247
+ if (!streamUserId || streamUserId === currentUserId) {
1248
+ setUserActionState(updater);
1249
+ }
1250
+ },
1251
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1252
+ []
1253
+ );
1279
1254
  const wrappedCallbacks = react.useMemo(() => ({
1280
1255
  ...callbacksRef.current,
1281
1256
  onMessageSent: (message) => callbacksRef.current.onMessageSent?.(message),
@@ -1285,14 +1260,14 @@ function useChatV2(config, callbacks = {}) {
1285
1260
  onExecutionTraceClick: (data) => callbacksRef.current.onExecutionTraceClick?.(data),
1286
1261
  onSessionIdChange: (sessionId) => callbacksRef.current.onSessionIdChange?.(sessionId),
1287
1262
  onUserActionRequired: (request) => {
1288
- setUserActionState((prev) => ({
1263
+ storeAwareSetUserActionState((prev) => ({
1289
1264
  ...prev,
1290
1265
  prompts: upsertPrompt(prev.prompts, request)
1291
1266
  }));
1292
1267
  callbacksRef.current.onUserActionRequired?.(request);
1293
1268
  },
1294
1269
  onUserNotification: (notification) => {
1295
- setUserActionState(
1270
+ storeAwareSetUserActionState(
1296
1271
  (prev) => prev.notifications.some((n) => n.id === notification.id) ? prev : { ...prev, notifications: [...prev.notifications, notification] }
1297
1272
  );
1298
1273
  callbacksRef.current.onUserNotification?.(notification);
@@ -1307,45 +1282,21 @@ function useChatV2(config, callbacks = {}) {
1307
1282
  );
1308
1283
  const sendMessage = react.useCallback(
1309
1284
  async (userMessage, options) => {
1310
- const trimmedMessage = userMessage.trim();
1311
- const files = options?.files ?? [];
1312
- const hasPreuploadedAttachments = (options?.attachments?.length ?? 0) > 0;
1313
- if (!trimmedMessage && files.length === 0 && !hasPreuploadedAttachments) return;
1314
- let streamAttachments = options?.attachments;
1315
- if (!streamAttachments && files.length > 0) {
1316
- setIsUploadingAttachments(true);
1317
- try {
1318
- streamAttachments = await uploadAttachments(configRef.current, files);
1319
- } catch (error) {
1320
- if (error.name !== "AbortError") {
1321
- callbacksRef.current.onError?.(error);
1322
- }
1323
- throw error;
1324
- } finally {
1325
- setIsUploadingAttachments(false);
1326
- }
1327
- }
1285
+ if (!userMessage.trim()) return;
1328
1286
  if (!sessionIdRef.current && configRef.current.autoGenerateSessionId !== false) {
1329
1287
  sessionIdRef.current = generateId();
1330
1288
  callbacksRef.current.onSessionIdChange?.(sessionIdRef.current);
1331
1289
  }
1332
- const messageAttachments = files.length > 0 ? buildMessageAttachments(files) : streamAttachments?.length ? streamAttachments.map((attachment, index) => ({
1333
- id: `att-${Date.now()}-${index}`,
1334
- filename: attachment.filename,
1335
- mimeType: attachment.mimeType,
1336
- kind: attachment.mimeType.startsWith("image/") ? "image" : "file"
1337
- })) : void 0;
1338
1290
  const userMessageId = `user-${Date.now()}`;
1339
1291
  const userMsg = {
1340
1292
  id: userMessageId,
1341
1293
  sessionId: sessionIdRef.current,
1342
1294
  role: "user",
1343
- content: trimmedMessage,
1344
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1345
- attachments: messageAttachments
1295
+ content: userMessage,
1296
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1346
1297
  };
1347
1298
  setMessages((prev) => [...prev, userMsg]);
1348
- callbacksRef.current.onMessageSent?.(trimmedMessage);
1299
+ callbacksRef.current.onMessageSent?.(userMessage);
1349
1300
  setIsWaitingForResponse(true);
1350
1301
  callbacksRef.current.onStreamStart?.();
1351
1302
  const streamingId = `assistant-${Date.now()}`;
@@ -1372,14 +1323,11 @@ function useChatV2(config, callbacks = {}) {
1372
1323
  activeStreamStore.start(userId, abortController, initialMessages);
1373
1324
  }
1374
1325
  const newSessionId = await startStream(
1375
- trimmedMessage,
1326
+ userMessage,
1376
1327
  streamingId,
1377
1328
  sessionIdRef.current,
1378
1329
  abortController,
1379
- {
1380
- analysisMode: options?.analysisMode,
1381
- attachments: streamAttachments
1382
- }
1330
+ options
1383
1331
  );
1384
1332
  const finalStreamUserId = streamUserIdRef.current ?? userId;
1385
1333
  if (finalStreamUserId) {
@@ -1409,7 +1357,7 @@ function useChatV2(config, callbacks = {}) {
1409
1357
  streamUserIdRef.current = void 0;
1410
1358
  cancelStreamManager();
1411
1359
  setIsWaitingForResponse(false);
1412
- setUserActionState((prev) => ({ ...prev, prompts: [] }));
1360
+ storeAwareSetUserActionState((prev) => ({ ...prev, prompts: [] }));
1413
1361
  setMessages(
1414
1362
  (prev) => prev.map((msg) => {
1415
1363
  if (msg.isStreaming) {
@@ -1424,7 +1372,7 @@ function useChatV2(config, callbacks = {}) {
1424
1372
  return msg;
1425
1373
  })
1426
1374
  );
1427
- }, [cancelStreamManager]);
1375
+ }, [cancelStreamManager, storeAwareSetUserActionState]);
1428
1376
  const resetSession = react.useCallback(() => {
1429
1377
  const streamUserId = streamUserIdRef.current ?? configRef.current.userId;
1430
1378
  if (streamUserId) {
@@ -1438,7 +1386,7 @@ function useChatV2(config, callbacks = {}) {
1438
1386
  sessionIdRef.current = void 0;
1439
1387
  abortControllerRef.current?.abort();
1440
1388
  setIsWaitingForResponse(false);
1441
- setUserActionState(EMPTY_USER_ACTION_STATE);
1389
+ storeAwareSetUserActionState(EMPTY_USER_ACTION_STATE2);
1442
1390
  }, []);
1443
1391
  const getSessionId = react.useCallback(() => {
1444
1392
  return sessionIdRef.current;
@@ -1448,21 +1396,21 @@ function useChatV2(config, callbacks = {}) {
1448
1396
  }, [messages]);
1449
1397
  const setPromptStatus = react.useCallback(
1450
1398
  (userActionId, status) => {
1451
- setUserActionState((prev) => ({
1399
+ storeAwareSetUserActionState((prev) => ({
1452
1400
  ...prev,
1453
1401
  prompts: prev.prompts.map(
1454
1402
  (p) => p.userActionId === userActionId ? { ...p, status } : p
1455
1403
  )
1456
1404
  }));
1457
1405
  },
1458
- []
1406
+ [storeAwareSetUserActionState]
1459
1407
  );
1460
1408
  const removePrompt = react.useCallback((userActionId) => {
1461
- setUserActionState((prev) => ({
1409
+ storeAwareSetUserActionState((prev) => ({
1462
1410
  ...prev,
1463
1411
  prompts: prev.prompts.filter((p) => p.userActionId !== userActionId)
1464
1412
  }));
1465
- }, []);
1413
+ }, [storeAwareSetUserActionState]);
1466
1414
  const submitUserAction2 = react.useCallback(
1467
1415
  async (userActionId, content) => {
1468
1416
  setPromptStatus(userActionId, "submitting");
@@ -1517,12 +1465,25 @@ function useChatV2(config, callbacks = {}) {
1517
1465
  },
1518
1466
  [setPromptStatus]
1519
1467
  );
1468
+ const expireUserAction2 = react.useCallback(
1469
+ async (userActionId) => {
1470
+ setPromptStatus(userActionId, "expired");
1471
+ try {
1472
+ await expireUserAction(configRef.current, userActionId);
1473
+ } catch (error) {
1474
+ if (error instanceof UserActionStaleError) return;
1475
+ callbacksRef.current.onError?.(error);
1476
+ throw error;
1477
+ }
1478
+ },
1479
+ [setPromptStatus]
1480
+ );
1520
1481
  const dismissNotification = react.useCallback((id) => {
1521
- setUserActionState((prev) => ({
1482
+ storeAwareSetUserActionState((prev) => ({
1522
1483
  ...prev,
1523
1484
  notifications: prev.notifications.filter((n) => n.id !== id)
1524
1485
  }));
1525
- }, []);
1486
+ }, [storeAwareSetUserActionState]);
1526
1487
  react.useEffect(() => {
1527
1488
  const prevSubscriptionUserId = subscriptionPrevUserIdRef.current;
1528
1489
  subscriptionPrevUserIdRef.current = config.userId;
@@ -1531,14 +1492,17 @@ function useChatV2(config, callbacks = {}) {
1531
1492
  if (prevSubscriptionUserId && prevSubscriptionUserId !== userId && streamUserIdRef.current === prevSubscriptionUserId && !activeStreamStore.has(prevSubscriptionUserId) && activeStreamStore.has(userId)) {
1532
1493
  streamUserIdRef.current = userId;
1533
1494
  }
1534
- const unsubscribe = activeStreamStore.subscribe(userId, (msgs, isWaiting) => {
1495
+ const unsubscribe = activeStreamStore.subscribe(userId, (msgs, isWaiting, actions) => {
1535
1496
  setMessages(msgs);
1536
1497
  setIsWaitingForResponse(isWaiting);
1498
+ setUserActionState(actions);
1537
1499
  });
1538
1500
  const active = activeStreamStore.get(userId);
1539
1501
  if (active) {
1502
+ streamUserIdRef.current = userId;
1540
1503
  setMessages(active.messages);
1541
1504
  setIsWaitingForResponse(active.isWaiting);
1505
+ setUserActionState(active.userActionState);
1542
1506
  }
1543
1507
  return unsubscribe;
1544
1508
  }, [config.userId]);
@@ -1557,18 +1521,6 @@ function useChatV2(config, callbacks = {}) {
1557
1521
  setMessages(config.initialMessages);
1558
1522
  sessionIdRef.current = getSessionIdFromMessages(config.initialMessages) ?? config.initialSessionId;
1559
1523
  }, [config.initialMessages, config.initialSessionId, config.userId]);
1560
- const hydratedSessionIdRef = react.useRef(void 0);
1561
- react.useEffect(() => {
1562
- if (config.userId) return;
1563
- if (!config.initialMessages?.length) return;
1564
- const sessionId = config.initialSessionId;
1565
- if (hydratedSessionIdRef.current === sessionId && messagesRef.current.length > 0) {
1566
- return;
1567
- }
1568
- hydratedSessionIdRef.current = sessionId;
1569
- setMessages(config.initialMessages);
1570
- sessionIdRef.current = getSessionIdFromMessages(config.initialMessages) ?? sessionId;
1571
- }, [config.initialMessages, config.initialSessionId, config.userId]);
1572
1524
  react.useEffect(() => {
1573
1525
  const prevUserId = prevUserIdRef.current;
1574
1526
  prevUserIdRef.current = config.userId;
@@ -1578,12 +1530,14 @@ function useChatV2(config, callbacks = {}) {
1578
1530
  setMessages([]);
1579
1531
  sessionIdRef.current = void 0;
1580
1532
  setIsWaitingForResponse(false);
1581
- setUserActionState(EMPTY_USER_ACTION_STATE);
1533
+ setUserActionState(EMPTY_USER_ACTION_STATE2);
1582
1534
  } else if (config.userId) {
1583
1535
  const active = activeStreamStore.get(config.userId);
1584
1536
  if (active) {
1537
+ streamUserIdRef.current = config.userId;
1585
1538
  setMessages(active.messages);
1586
1539
  setIsWaitingForResponse(active.isWaiting);
1540
+ setUserActionState(active.userActionState);
1587
1541
  sessionIdRef.current = getSessionIdFromMessages(active.messages) ?? config.initialSessionId;
1588
1542
  return;
1589
1543
  }
@@ -1603,12 +1557,12 @@ function useChatV2(config, callbacks = {}) {
1603
1557
  getSessionId,
1604
1558
  getMessages,
1605
1559
  isWaitingForResponse,
1606
- isUploadingAttachments,
1607
1560
  sessionId: sessionIdRef.current,
1608
1561
  userActionState,
1609
1562
  submitUserAction: submitUserAction2,
1610
1563
  cancelUserAction: cancelUserAction2,
1611
1564
  resendUserAction: resendUserAction2,
1565
+ expireUserAction: expireUserAction2,
1612
1566
  dismissNotification
1613
1567
  };
1614
1568
  }
@@ -2571,35 +2525,37 @@ function uaPalette(isDark) {
2571
2525
  grabber: "rgba(15,23,42,0.18)"
2572
2526
  };
2573
2527
  }
2574
- function promptInitialSeconds(prompt) {
2575
- return prompt && typeof prompt.expirySeconds === "number" && prompt.expirySeconds > 0 ? Math.floor(prompt.expirySeconds) : void 0;
2576
- }
2577
2528
  function promptTimerKey(prompt) {
2578
- return prompt ? `${prompt.userActionId}|${prompt.subAction ?? ""}` : "";
2529
+ return prompt ? `${prompt.userActionId}|${prompt.subAction ?? ""}|${prompt.expiresAt ?? ""}` : "";
2579
2530
  }
2580
2531
  function useExpiredFlag(prompt) {
2581
- const initial = promptInitialSeconds(prompt);
2582
2532
  const key = promptTimerKey(prompt);
2583
- const [expired, setExpired] = react.useState(false);
2533
+ const [expired, setExpired] = react.useState(() => prompt ? isUserActionExpired(prompt) : false);
2584
2534
  react.useEffect(() => {
2585
- setExpired(false);
2586
- if (initial === void 0) return;
2587
- const t = setTimeout(() => setExpired(true), initial * 1e3);
2535
+ const secondsLeft = prompt ? getUserActionSecondsLeft(prompt) : void 0;
2536
+ setExpired(secondsLeft !== void 0 && secondsLeft <= 0);
2537
+ if (secondsLeft === void 0 || secondsLeft <= 0) return;
2538
+ const t = setTimeout(() => setExpired(true), secondsLeft * 1e3);
2588
2539
  return () => clearTimeout(t);
2589
- }, [key, initial]);
2540
+ }, [key, prompt]);
2590
2541
  return expired;
2591
2542
  }
2592
- function useSecondsLeft(expirySeconds, restartKey) {
2593
- const initial = typeof expirySeconds === "number" && expirySeconds > 0 ? Math.floor(expirySeconds) : void 0;
2594
- const [left, setLeft] = react.useState(initial);
2543
+ function useSecondsLeft(prompt, restartKey) {
2544
+ const deadlineMs = prompt && typeof prompt.expiresAt === "number" && prompt.expiresAt > 0 ? prompt.expiresAt : prompt && typeof prompt.expirySeconds === "number" && prompt.expirySeconds > 0 ? Date.now() + Math.floor(prompt.expirySeconds) * 1e3 : void 0;
2545
+ const [left, setLeft] = react.useState(
2546
+ () => deadlineMs !== void 0 ? Math.max(0, Math.ceil((deadlineMs - Date.now()) / 1e3)) : void 0
2547
+ );
2595
2548
  react.useEffect(() => {
2596
- setLeft(initial);
2597
- if (initial === void 0) return;
2598
- const id = setInterval(() => {
2599
- setLeft((s2) => s2 === void 0 ? s2 : s2 <= 1 ? 0 : s2 - 1);
2600
- }, 1e3);
2549
+ if (deadlineMs === void 0) {
2550
+ setLeft(void 0);
2551
+ return;
2552
+ }
2553
+ const remaining = () => Math.max(0, Math.ceil((deadlineMs - Date.now()) / 1e3));
2554
+ const sync = () => setLeft(remaining());
2555
+ sync();
2556
+ const id = setInterval(sync, 1e3);
2601
2557
  return () => clearInterval(id);
2602
- }, [restartKey, initial]);
2558
+ }, [restartKey, deadlineMs]);
2603
2559
  return left;
2604
2560
  }
2605
2561
  function SheetTimerPill({
@@ -2607,7 +2563,7 @@ function SheetTimerPill({
2607
2563
  accent,
2608
2564
  isDark
2609
2565
  }) {
2610
- const left = useSecondsLeft(prompt.expirySeconds, promptTimerKey(prompt));
2566
+ const left = useSecondsLeft(prompt, promptTimerKey(prompt));
2611
2567
  if (left === void 0 || prompt.status === "stale") return null;
2612
2568
  const expired = left <= 0;
2613
2569
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -2629,7 +2585,7 @@ function CardTimerPill({
2629
2585
  accent,
2630
2586
  isDark
2631
2587
  }) {
2632
- const left = useSecondsLeft(prompt.expirySeconds, promptTimerKey(prompt));
2588
+ const left = useSecondsLeft(prompt, promptTimerKey(prompt));
2633
2589
  if (left === void 0 || left <= 0) return null;
2634
2590
  return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [rc.timer, { backgroundColor: accent + (isDark ? "26" : "14") }], children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: { color: accent, fontSize: 12, fontWeight: "700" }, children: [
2635
2591
  left,
@@ -2695,6 +2651,7 @@ function VerificationBody({
2695
2651
  expired,
2696
2652
  pal,
2697
2653
  accent,
2654
+ isDark,
2698
2655
  onSubmit,
2699
2656
  onCancel,
2700
2657
  onResend
@@ -2757,7 +2714,11 @@ function VerificationBody({
2757
2714
  } catch {
2758
2715
  }
2759
2716
  }, [locked, onResend, prompt.userActionId, resendSec]);
2760
- const description = prompt.message?.trim() || (isNumeric ? `Enter the ${codeLen}-digit code to continue` : "Enter the verification code to continue");
2717
+ const promptMessage = prompt.message?.trim();
2718
+ const description = promptMessage || (isNumeric ? `Enter the ${codeLen}-digit code to continue` : "Enter the verification code to continue");
2719
+ const renderMarkdown = !!promptMessage && prompt.metadata?.["payman/messageFormat"] === "markdown";
2720
+ const mdStyles = isDark ? MD_STYLES_DARK : MD_STYLES_LIGHT;
2721
+ const mdRules = react.useMemo(() => makeMdRules(isDark, accent), [isDark, accent]);
2761
2722
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: ub.bodyWrap, children: [
2762
2723
  /* @__PURE__ */ jsxRuntime.jsxs(
2763
2724
  reactNative.ScrollView,
@@ -2767,7 +2728,7 @@ function VerificationBody({
2767
2728
  keyboardShouldPersistTaps: "handled",
2768
2729
  showsVerticalScrollIndicator: false,
2769
2730
  children: [
2770
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.desc, { color: pal.muted }], children: description }),
2731
+ renderMarkdown ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ub.messageBlock, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown__default.default, { style: mdStyles, rules: mdRules, children: description.replace(/\\n/g, "\n") }) }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.desc, { color: pal.muted }], children: description }),
2771
2732
  isNumeric ? /* @__PURE__ */ jsxRuntime.jsx(
2772
2733
  CodeInput,
2773
2734
  {
@@ -3106,6 +3067,7 @@ function UserActionSheet({
3106
3067
  expired,
3107
3068
  pal,
3108
3069
  accent,
3070
+ isDark,
3109
3071
  onSubmit,
3110
3072
  onCancel,
3111
3073
  onResend