chat 4.26.0 → 4.28.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.
Files changed (50) hide show
  1. package/dist/{chunk-OPV5U4WG.js → chunk-V25FKIIL.js} +44 -1
  2. package/dist/index.d.ts +485 -33
  3. package/dist/index.js +862 -135
  4. package/dist/{jsx-runtime-DxATbnrP.d.ts → jsx-runtime-DxGwoLu2.d.ts} +49 -5
  5. package/dist/jsx-runtime.d.ts +1 -1
  6. package/dist/jsx-runtime.js +1 -1
  7. package/docs/actions.mdx +52 -1
  8. package/docs/adapters.mdx +43 -37
  9. package/docs/api/cards.mdx +4 -0
  10. package/docs/api/chat.mdx +172 -6
  11. package/docs/api/index.mdx +2 -0
  12. package/docs/api/markdown.mdx +28 -5
  13. package/docs/api/message.mdx +58 -1
  14. package/docs/api/meta.json +2 -0
  15. package/docs/api/modals.mdx +50 -0
  16. package/docs/api/postable-message.mdx +55 -1
  17. package/docs/api/thread.mdx +33 -3
  18. package/docs/api/transcripts.mdx +220 -0
  19. package/docs/cards.mdx +6 -0
  20. package/docs/concurrency.mdx +4 -0
  21. package/docs/contributing/building.mdx +73 -1
  22. package/docs/contributing/publishing.mdx +33 -0
  23. package/docs/conversation-history.mdx +137 -0
  24. package/docs/direct-messages.mdx +13 -4
  25. package/docs/ephemeral-messages.mdx +1 -1
  26. package/docs/error-handling.mdx +15 -3
  27. package/docs/files.mdx +2 -1
  28. package/docs/getting-started.mdx +1 -11
  29. package/docs/index.mdx +7 -5
  30. package/docs/meta.json +14 -5
  31. package/docs/modals.mdx +97 -1
  32. package/docs/posting-messages.mdx +7 -3
  33. package/docs/streaming.mdx +74 -18
  34. package/docs/subject.mdx +53 -0
  35. package/docs/threads-messages-channels.mdx +43 -0
  36. package/docs/usage.mdx +11 -2
  37. package/package.json +3 -2
  38. package/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md +180 -0
  39. package/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md +134 -0
  40. package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +220 -0
  41. package/resources/guides/run-and-track-deploys-from-slack.md +270 -0
  42. package/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md +147 -0
  43. package/resources/guides/triage-form-submissions-with-chat-sdk.md +178 -0
  44. package/resources/templates.json +19 -0
  45. package/docs/guides/code-review-hono.mdx +0 -241
  46. package/docs/guides/discord-nuxt.mdx +0 -227
  47. package/docs/guides/durable-chat-sessions-nextjs.mdx +0 -337
  48. package/docs/guides/meta.json +0 -10
  49. package/docs/guides/scheduled-posts-neon.mdx +0 -447
  50. package/docs/guides/slack-nextjs.mdx +0 -234
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  CardLink,
7
7
  CardText,
8
8
  Divider,
9
+ ExternalSelect,
9
10
  Field,
10
11
  Fields,
11
12
  Image,
@@ -59,7 +60,7 @@ import {
59
60
  toModalElement,
60
61
  toPlainText,
61
62
  walkAst
62
- } from "./chunk-OPV5U4WG.js";
63
+ } from "./chunk-V25FKIIL.js";
63
64
 
64
65
  // src/ai.ts
65
66
  var TEXT_MIME_PREFIXES = [
@@ -189,6 +190,127 @@ ${linkParts}`;
189
190
  // src/channel.ts
190
191
  import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE2, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE2 } from "@workflow/serde";
191
192
 
193
+ // src/callback-url.ts
194
+ var CALLBACK_TOKEN_PREFIX = "__cb:";
195
+ var CALLBACK_CACHE_KEY_PREFIX = "chat:callback:";
196
+ var CALLBACK_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
197
+ function encodeCallbackValue(token) {
198
+ return `${CALLBACK_TOKEN_PREFIX}${token}`;
199
+ }
200
+ function decodeCallbackValue(value) {
201
+ if (!value?.startsWith(CALLBACK_TOKEN_PREFIX)) {
202
+ return { callbackToken: void 0 };
203
+ }
204
+ return { callbackToken: value.slice(CALLBACK_TOKEN_PREFIX.length) };
205
+ }
206
+ function generateToken() {
207
+ return crypto.randomUUID().replace(/-/g, "").slice(0, 16);
208
+ }
209
+ async function processActionsElement(actions, stateAdapter) {
210
+ return {
211
+ type: "actions",
212
+ children: await Promise.all(
213
+ actions.children.map(async (el) => {
214
+ if (el.type !== "button" || !el.callbackUrl) {
215
+ return el;
216
+ }
217
+ const token = generateToken();
218
+ const stored = {
219
+ url: el.callbackUrl,
220
+ originalValue: el.value
221
+ };
222
+ await stateAdapter.set(
223
+ `${CALLBACK_CACHE_KEY_PREFIX}${token}`,
224
+ stored,
225
+ CALLBACK_TTL_MS
226
+ );
227
+ const processed = {
228
+ type: "button",
229
+ id: el.id,
230
+ label: el.label,
231
+ style: el.style,
232
+ disabled: el.disabled,
233
+ value: encodeCallbackValue(token),
234
+ actionType: el.actionType
235
+ };
236
+ return processed;
237
+ })
238
+ )
239
+ };
240
+ }
241
+ function hasCallbackButtons(children) {
242
+ for (const child of children) {
243
+ if (child.type === "actions") {
244
+ for (const el of child.children) {
245
+ if (el.type === "button" && el.callbackUrl) {
246
+ return true;
247
+ }
248
+ }
249
+ }
250
+ if (child.type === "section" && "children" in child && hasCallbackButtons(child.children)) {
251
+ return true;
252
+ }
253
+ }
254
+ return false;
255
+ }
256
+ async function processChildren(children, stateAdapter) {
257
+ const result = [];
258
+ for (const child of children) {
259
+ if (child.type === "actions") {
260
+ result.push(await processActionsElement(child, stateAdapter));
261
+ } else if (child.type === "section" && "children" in child) {
262
+ result.push({
263
+ ...child,
264
+ children: await processChildren(child.children, stateAdapter)
265
+ });
266
+ } else {
267
+ result.push(child);
268
+ }
269
+ }
270
+ return result;
271
+ }
272
+ async function processCardCallbackUrls(card, stateAdapter) {
273
+ if (!hasCallbackButtons(card.children)) {
274
+ return card;
275
+ }
276
+ return {
277
+ ...card,
278
+ children: await processChildren(card.children, stateAdapter)
279
+ };
280
+ }
281
+ async function resolveCallbackUrl(token, stateAdapter) {
282
+ const stored = await stateAdapter.get(
283
+ `${CALLBACK_CACHE_KEY_PREFIX}${token}`
284
+ );
285
+ if (!stored) {
286
+ return null;
287
+ }
288
+ if (typeof stored === "string") {
289
+ return { url: stored };
290
+ }
291
+ return stored;
292
+ }
293
+ async function postToCallbackUrl(callbackUrl, payload) {
294
+ try {
295
+ const response = await fetch(callbackUrl, {
296
+ method: "POST",
297
+ headers: { "Content-Type": "application/json" },
298
+ body: JSON.stringify(payload)
299
+ });
300
+ if (!response.ok) {
301
+ return {
302
+ error: new Error(
303
+ `Callback URL returned ${response.status}: ${await response.text().catch(() => "")}`
304
+ ),
305
+ status: response.status
306
+ };
307
+ }
308
+ return { status: response.status };
309
+ } catch (error) {
310
+ return { error };
311
+ }
312
+ }
313
+
192
314
  // src/chat-singleton.ts
193
315
  var _singleton = null;
194
316
  function setChatSingleton(chat) {
@@ -244,6 +366,10 @@ async function* fromFullStream(stream) {
244
366
 
245
367
  // src/message.ts
246
368
  import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";
369
+ var adapterMap = /* @__PURE__ */ new WeakMap();
370
+ function setMessageAdapter(message, adapter) {
371
+ adapterMap.set(message, adapter);
372
+ }
247
373
  var Message = class _Message {
248
374
  /** Unique message ID */
249
375
  id;
@@ -282,8 +408,31 @@ var Message = class _Message {
282
408
  * ```
283
409
  */
284
410
  isMention;
411
+ /**
412
+ * Cross-platform user key for this message's author.
413
+ *
414
+ * Set by the Chat SDK before passing the message to handlers, when
415
+ * `ChatConfig.identity` is configured. `undefined` if no resolver is
416
+ * configured; `undefined` (i.e. absent) when the resolver returned null.
417
+ *
418
+ * Used by the Transcripts API to look up / append per-user transcripts.
419
+ */
420
+ userKey;
285
421
  /** Links found in the message */
286
422
  links;
423
+ _subjectPromise;
424
+ get subject() {
425
+ if (this._subjectPromise) {
426
+ return this._subjectPromise;
427
+ }
428
+ const adapter = adapterMap.get(this);
429
+ if (!adapter?.fetchSubject) {
430
+ this._subjectPromise = Promise.resolve(null);
431
+ return this._subjectPromise;
432
+ }
433
+ this._subjectPromise = adapter.fetchSubject(this.raw).catch(() => null);
434
+ return this._subjectPromise;
435
+ }
287
436
  constructor(data) {
288
437
  this.id = data.id;
289
438
  this.threadId = data.threadId;
@@ -330,7 +479,8 @@ var Message = class _Message {
330
479
  mimeType: att.mimeType,
331
480
  size: att.size,
332
481
  width: att.width,
333
- height: att.height
482
+ height: att.height,
483
+ fetchMetadata: att.fetchMetadata
334
484
  })),
335
485
  isMention: this.isMention,
336
486
  links: this.links.length > 0 ? this.links.map((link2) => ({
@@ -495,7 +645,7 @@ var ChannelImpl = class _ChannelImpl {
495
645
  _adapterName;
496
646
  _stateAdapterInstance;
497
647
  _name = null;
498
- _messageHistory;
648
+ _threadHistory;
499
649
  constructor(config) {
500
650
  this.id = config.id;
501
651
  this.isDM = config.isDM ?? false;
@@ -505,7 +655,7 @@ var ChannelImpl = class _ChannelImpl {
505
655
  } else {
506
656
  this._adapter = config.adapter;
507
657
  this._stateAdapterInstance = config.stateAdapter;
508
- this._messageHistory = config.messageHistory;
658
+ this._threadHistory = config.threadHistory;
509
659
  }
510
660
  }
511
661
  get adapter() {
@@ -559,7 +709,7 @@ var ChannelImpl = class _ChannelImpl {
559
709
  get messages() {
560
710
  const adapter = this.adapter;
561
711
  const channelId = this.id;
562
- const messageHistory = this._messageHistory;
712
+ const threadHistory = this._threadHistory;
563
713
  return {
564
714
  async *[Symbol.asyncIterator]() {
565
715
  let cursor;
@@ -577,8 +727,8 @@ var ChannelImpl = class _ChannelImpl {
577
727
  }
578
728
  cursor = result.nextCursor;
579
729
  }
580
- if (!yieldedAny && messageHistory) {
581
- const cached = await messageHistory.getMessages(channelId);
730
+ if (!yieldedAny && threadHistory) {
731
+ const cached = await threadHistory.getMessages(channelId);
582
732
  for (let i = cached.length - 1; i >= 0; i--) {
583
733
  yield cached[i];
584
734
  }
@@ -635,6 +785,8 @@ var ChannelImpl = class _ChannelImpl {
635
785
  for await (const chunk of fromFullStream(message)) {
636
786
  if (typeof chunk === "string") {
637
787
  accumulated += chunk;
788
+ } else if (chunk.type === "markdown_text") {
789
+ accumulated += chunk.text;
638
790
  }
639
791
  }
640
792
  return this.postSingleMessage({ markdown: accumulated });
@@ -647,6 +799,7 @@ var ChannelImpl = class _ChannelImpl {
647
799
  }
648
800
  postable = card;
649
801
  }
802
+ postable = await this.processCallbackUrls(postable);
650
803
  return this.postSingleMessage(postable);
651
804
  }
652
805
  async handlePostableObject(obj) {
@@ -664,8 +817,8 @@ var ChannelImpl = class _ChannelImpl {
664
817
  postable,
665
818
  rawMessage.threadId
666
819
  );
667
- if (this._messageHistory) {
668
- await this._messageHistory.append(this.id, new Message(sent));
820
+ if (this._threadHistory) {
821
+ await this._threadHistory.append(this.id, new Message(sent));
669
822
  }
670
823
  return sent;
671
824
  }
@@ -682,6 +835,7 @@ var ChannelImpl = class _ChannelImpl {
682
835
  } else {
683
836
  postable = message;
684
837
  }
838
+ postable = await this.processCallbackUrls(postable);
685
839
  if (this.adapter.postEphemeral) {
686
840
  return this.adapter.postEphemeral(this.id, userId, postable);
687
841
  }
@@ -711,6 +865,7 @@ var ChannelImpl = class _ChannelImpl {
711
865
  } else {
712
866
  postable = message;
713
867
  }
868
+ postable = await this.processCallbackUrls(postable);
714
869
  if (!this.adapter.scheduleMessage) {
715
870
  throw new NotImplementedError(
716
871
  "Scheduled messages are not supported by this adapter",
@@ -719,6 +874,24 @@ var ChannelImpl = class _ChannelImpl {
719
874
  }
720
875
  return this.adapter.scheduleMessage(this.id, postable, options);
721
876
  }
877
+ async processCallbackUrls(postable) {
878
+ if (typeof postable === "string") {
879
+ return postable;
880
+ }
881
+ if ("type" in postable && postable.type === "card") {
882
+ return processCardCallbackUrls(postable, this._stateAdapter);
883
+ }
884
+ if ("card" in postable && postable.card?.type === "card") {
885
+ const processed = await processCardCallbackUrls(
886
+ postable.card,
887
+ this._stateAdapter
888
+ );
889
+ if (processed !== postable.card) {
890
+ return { ...postable, card: processed };
891
+ }
892
+ }
893
+ return postable;
894
+ }
722
895
  async startTyping(status) {
723
896
  await this.adapter.startTyping(this.id, status);
724
897
  }
@@ -788,6 +961,7 @@ var ChannelImpl = class _ChannelImpl {
788
961
  }
789
962
  editPostable = card;
790
963
  }
964
+ editPostable = await self.processCallbackUrls(editPostable);
791
965
  await adapter.editMessage(threadId, messageId, editPostable);
792
966
  return self.createSentMessage(messageId, editPostable);
793
967
  },
@@ -856,46 +1030,6 @@ function extractMessageContent(message) {
856
1030
  throw new Error("Invalid PostableMessage format");
857
1031
  }
858
1032
 
859
- // src/message-history.ts
860
- var DEFAULT_MAX_MESSAGES = 100;
861
- var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
862
- var KEY_PREFIX = "msg-history:";
863
- var MessageHistoryCache = class {
864
- state;
865
- maxMessages;
866
- ttlMs;
867
- constructor(state, config) {
868
- this.state = state;
869
- this.maxMessages = config?.maxMessages ?? DEFAULT_MAX_MESSAGES;
870
- this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
871
- }
872
- /**
873
- * Atomically append a message to the history for a thread.
874
- * Trims to maxMessages (keeps newest) and refreshes TTL.
875
- */
876
- async append(threadId, message) {
877
- const key = `${KEY_PREFIX}${threadId}`;
878
- const serialized = message.toJSON();
879
- serialized.raw = null;
880
- await this.state.appendToList(key, serialized, {
881
- maxLength: this.maxMessages,
882
- ttlMs: this.ttlMs
883
- });
884
- }
885
- /**
886
- * Get messages for a thread in chronological order (oldest first).
887
- *
888
- * @param threadId - The thread ID
889
- * @param limit - Optional limit on number of messages to return (returns newest N)
890
- */
891
- async getMessages(threadId, limit) {
892
- const key = `${KEY_PREFIX}${threadId}`;
893
- const stored = await this.state.getList(key);
894
- const sliced = limit && stored.length > limit ? stored.slice(stored.length - limit) : stored;
895
- return sliced.map((s) => Message.fromJSON(s));
896
- }
897
- };
898
-
899
1033
  // src/thread.ts
900
1034
  import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE3, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE3 } from "@workflow/serde";
901
1035
 
@@ -1166,8 +1300,8 @@ var ThreadImpl = class _ThreadImpl {
1166
1300
  _fallbackStreamingPlaceholderText;
1167
1301
  /** Cached channel instance */
1168
1302
  _channel;
1169
- /** Message history cache (set only for adapters with persistMessageHistory) */
1170
- _messageHistory;
1303
+ /** Thread history cache (set only for adapters with persistThreadHistory) */
1304
+ _threadHistory;
1171
1305
  _logger;
1172
1306
  constructor(config) {
1173
1307
  this.id = config.id;
@@ -1184,7 +1318,7 @@ var ThreadImpl = class _ThreadImpl {
1184
1318
  } else {
1185
1319
  this._adapter = config.adapter;
1186
1320
  this._stateAdapterInstance = config.stateAdapter;
1187
- this._messageHistory = config.messageHistory;
1321
+ this._threadHistory = config.threadHistory;
1188
1322
  }
1189
1323
  if (config.initialMessage) {
1190
1324
  this._recentMessages = [config.initialMessage];
@@ -1265,7 +1399,7 @@ var ThreadImpl = class _ThreadImpl {
1265
1399
  stateAdapter: this._stateAdapter,
1266
1400
  isDM: this.isDM,
1267
1401
  channelVisibility: this.channelVisibility,
1268
- messageHistory: this._messageHistory
1402
+ threadHistory: this._threadHistory
1269
1403
  });
1270
1404
  }
1271
1405
  return this._channel;
@@ -1277,7 +1411,7 @@ var ThreadImpl = class _ThreadImpl {
1277
1411
  get messages() {
1278
1412
  const adapter = this.adapter;
1279
1413
  const threadId = this.id;
1280
- const messageHistory = this._messageHistory;
1414
+ const threadHistory = this._threadHistory;
1281
1415
  return {
1282
1416
  async *[Symbol.asyncIterator]() {
1283
1417
  let cursor;
@@ -1297,8 +1431,8 @@ var ThreadImpl = class _ThreadImpl {
1297
1431
  }
1298
1432
  cursor = result.nextCursor;
1299
1433
  }
1300
- if (!yieldedAny && messageHistory) {
1301
- const cached = await messageHistory.getMessages(threadId);
1434
+ if (!yieldedAny && threadHistory) {
1435
+ const cached = await threadHistory.getMessages(threadId);
1302
1436
  for (let i = cached.length - 1; i >= 0; i--) {
1303
1437
  yield cached[i];
1304
1438
  }
@@ -1309,7 +1443,7 @@ var ThreadImpl = class _ThreadImpl {
1309
1443
  get allMessages() {
1310
1444
  const adapter = this.adapter;
1311
1445
  const threadId = this.id;
1312
- const messageHistory = this._messageHistory;
1446
+ const threadHistory = this._threadHistory;
1313
1447
  return {
1314
1448
  async *[Symbol.asyncIterator]() {
1315
1449
  let cursor;
@@ -1329,8 +1463,8 @@ var ThreadImpl = class _ThreadImpl {
1329
1463
  }
1330
1464
  cursor = result.nextCursor;
1331
1465
  }
1332
- if (!yieldedAny && messageHistory) {
1333
- const cached = await messageHistory.getMessages(threadId);
1466
+ if (!yieldedAny && threadHistory) {
1467
+ const cached = await threadHistory.getMessages(threadId);
1334
1468
  for (const message of cached) {
1335
1469
  yield message;
1336
1470
  }
@@ -1338,6 +1472,19 @@ var ThreadImpl = class _ThreadImpl {
1338
1472
  }
1339
1473
  };
1340
1474
  }
1475
+ async getParticipants() {
1476
+ const seen = /* @__PURE__ */ new Map();
1477
+ if (this._currentMessage && !this._currentMessage.author.isMe && !this._currentMessage.author.isBot) {
1478
+ seen.set(this._currentMessage.author.userId, this._currentMessage.author);
1479
+ }
1480
+ for await (const message of this.allMessages) {
1481
+ if (message.author.isMe || message.author.isBot || seen.has(message.author.userId)) {
1482
+ continue;
1483
+ }
1484
+ seen.set(message.author.userId, message.author);
1485
+ }
1486
+ return [...seen.values()];
1487
+ }
1341
1488
  async isSubscribed() {
1342
1489
  if (this._isSubscribedContext) {
1343
1490
  return true;
@@ -1355,6 +1502,16 @@ var ThreadImpl = class _ThreadImpl {
1355
1502
  }
1356
1503
  async post(message) {
1357
1504
  if (isPostableObject(message)) {
1505
+ if (message.kind === "stream") {
1506
+ const data = message.getPostData();
1507
+ const streamOptions = {
1508
+ ...data.options.updateIntervalMs ? { updateIntervalMs: data.options.updateIntervalMs } : {},
1509
+ ...data.options.groupTasks ? { taskDisplayMode: data.options.groupTasks } : {},
1510
+ ...data.options.endWith ? { stopBlocks: data.options.endWith } : {}
1511
+ };
1512
+ await this.handleStream(data.stream, streamOptions);
1513
+ return message;
1514
+ }
1358
1515
  await this.handlePostableObject(message);
1359
1516
  return message;
1360
1517
  }
@@ -1369,14 +1526,15 @@ var ThreadImpl = class _ThreadImpl {
1369
1526
  }
1370
1527
  postable = card;
1371
1528
  }
1529
+ postable = await this.processCallbackUrls(postable);
1372
1530
  const rawMessage = await this.adapter.postMessage(this.id, postable);
1373
1531
  const result = this.createSentMessage(
1374
1532
  rawMessage.id,
1375
1533
  postable,
1376
1534
  rawMessage.threadId
1377
1535
  );
1378
- if (this._messageHistory) {
1379
- await this._messageHistory.append(this.id, new Message(result));
1536
+ if (this._threadHistory) {
1537
+ await this._threadHistory.append(this.id, new Message(result));
1380
1538
  }
1381
1539
  return result;
1382
1540
  }
@@ -1402,6 +1560,7 @@ var ThreadImpl = class _ThreadImpl {
1402
1560
  } else {
1403
1561
  postable = message;
1404
1562
  }
1563
+ postable = await this.processCallbackUrls(postable);
1405
1564
  if (this.adapter.postEphemeral) {
1406
1565
  return this.adapter.postEphemeral(this.id, userId, postable);
1407
1566
  }
@@ -1420,6 +1579,24 @@ var ThreadImpl = class _ThreadImpl {
1420
1579
  }
1421
1580
  return null;
1422
1581
  }
1582
+ async processCallbackUrls(postable) {
1583
+ if (typeof postable === "string") {
1584
+ return postable;
1585
+ }
1586
+ if ("type" in postable && postable.type === "card") {
1587
+ return processCardCallbackUrls(postable, this._stateAdapter);
1588
+ }
1589
+ if ("card" in postable && postable.card?.type === "card") {
1590
+ const processed = await processCardCallbackUrls(
1591
+ postable.card,
1592
+ this._stateAdapter
1593
+ );
1594
+ if (processed !== postable.card) {
1595
+ return { ...postable, card: processed };
1596
+ }
1597
+ }
1598
+ return postable;
1599
+ }
1423
1600
  async schedule(message, options) {
1424
1601
  let postable;
1425
1602
  if (isJSX(message)) {
@@ -1431,6 +1608,9 @@ var ThreadImpl = class _ThreadImpl {
1431
1608
  } else {
1432
1609
  postable = message;
1433
1610
  }
1611
+ postable = await this.processCallbackUrls(
1612
+ postable
1613
+ );
1434
1614
  if (!this.adapter.scheduleMessage) {
1435
1615
  throw new NotImplementedError(
1436
1616
  "Scheduled messages are not supported by this adapter",
@@ -1442,15 +1622,16 @@ var ThreadImpl = class _ThreadImpl {
1442
1622
  /**
1443
1623
  * Handle streaming from an AsyncIterable.
1444
1624
  * Normalizes the stream (supports both textStream and fullStream from AI SDK),
1445
- * then uses adapter's native streaming if available, otherwise falls back to post+edit.
1625
+ * then uses the adapter's stream implementation if available, otherwise falls back to post+edit.
1446
1626
  */
1447
- async handleStream(rawStream) {
1627
+ async handleStream(rawStream, callerOptions) {
1448
1628
  const textStream = fromFullStream(rawStream);
1449
- const options = {};
1629
+ const options = { ...callerOptions };
1450
1630
  if (this._currentMessage) {
1451
1631
  options.recipientUserId = this._currentMessage.author.userId;
1452
- const raw = this._currentMessage.raw;
1453
- options.recipientTeamId = raw?.team_id ?? raw?.team;
1632
+ options.recipientTeamId = this.extractSlackRecipientTeamId(
1633
+ this._currentMessage.raw
1634
+ );
1454
1635
  }
1455
1636
  if (this.adapter.stream) {
1456
1637
  let accumulated = "";
@@ -1479,8 +1660,8 @@ var ThreadImpl = class _ThreadImpl {
1479
1660
  { markdown: accumulated },
1480
1661
  raw.threadId
1481
1662
  );
1482
- if (this._messageHistory) {
1483
- await this._messageHistory.append(this.id, new Message(sent));
1663
+ if (this._threadHistory) {
1664
+ await this._threadHistory.append(this.id, new Message(sent));
1484
1665
  }
1485
1666
  return sent;
1486
1667
  }
@@ -1508,6 +1689,31 @@ var ThreadImpl = class _ThreadImpl {
1508
1689
  };
1509
1690
  return this.fallbackStream(textOnlyStream, options);
1510
1691
  }
1692
+ /**
1693
+ * Slack payloads carry the workspace ID in a few different shapes depending on
1694
+ * the webhook type:
1695
+ * - Message events: `team_id` or `team` as a string
1696
+ * - `block_actions` payloads: `team.id` (object), with `user.team_id` as a fallback
1697
+ */
1698
+ extractSlackRecipientTeamId(raw) {
1699
+ if (!raw || typeof raw !== "object") {
1700
+ return void 0;
1701
+ }
1702
+ const payload = raw;
1703
+ if (typeof payload.team_id === "string" && payload.team_id) {
1704
+ return payload.team_id;
1705
+ }
1706
+ if (typeof payload.team === "string" && payload.team) {
1707
+ return payload.team;
1708
+ }
1709
+ if (payload.team && typeof payload.team === "object" && typeof payload.team.id === "string" && payload.team.id) {
1710
+ return payload.team.id;
1711
+ }
1712
+ if (typeof payload.user?.team_id === "string" && payload.user.team_id) {
1713
+ return payload.user.team_id;
1714
+ }
1715
+ return void 0;
1716
+ }
1511
1717
  async startTyping(status) {
1512
1718
  await this.adapter.startTyping(this.id, status);
1513
1719
  }
@@ -1602,8 +1808,8 @@ var ThreadImpl = class _ThreadImpl {
1602
1808
  { markdown: accumulated },
1603
1809
  threadIdForEdits
1604
1810
  );
1605
- if (this._messageHistory) {
1606
- await this._messageHistory.append(this.id, new Message(sent));
1811
+ if (this._threadHistory) {
1812
+ await this._threadHistory.append(this.id, new Message(sent));
1607
1813
  }
1608
1814
  return sent;
1609
1815
  }
@@ -1611,11 +1817,8 @@ var ThreadImpl = class _ThreadImpl {
1611
1817
  const result = await this.adapter.fetchMessages(this.id, { limit: 50 });
1612
1818
  if (result.messages.length > 0) {
1613
1819
  this._recentMessages = result.messages;
1614
- } else if (this._messageHistory) {
1615
- this._recentMessages = await this._messageHistory.getMessages(
1616
- this.id,
1617
- 50
1618
- );
1820
+ } else if (this._threadHistory) {
1821
+ this._recentMessages = await this._threadHistory.getMessages(this.id, 50);
1619
1822
  } else {
1620
1823
  this._recentMessages = [];
1621
1824
  }
@@ -1727,6 +1930,7 @@ var ThreadImpl = class _ThreadImpl {
1727
1930
  }
1728
1931
  postable2 = card;
1729
1932
  }
1933
+ postable2 = await self.processCallbackUrls(postable2);
1730
1934
  await adapter.editMessage(threadId, messageId, postable2);
1731
1935
  return self.createSentMessage(messageId, postable2);
1732
1936
  },
@@ -1770,6 +1974,7 @@ var ThreadImpl = class _ThreadImpl {
1770
1974
  }
1771
1975
  postable = card;
1772
1976
  }
1977
+ postable = await self.processCallbackUrls(postable);
1773
1978
  await adapter.editMessage(threadId, messageId, postable);
1774
1979
  return self.createSentMessage(messageId, postable, threadId);
1775
1980
  },
@@ -1851,13 +2056,186 @@ function reviver(_key, value) {
1851
2056
  return value;
1852
2057
  }
1853
2058
 
2059
+ // src/thread-history.ts
2060
+ var DEFAULT_MAX_MESSAGES = 100;
2061
+ var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
2062
+ var KEY_PREFIX = "msg-history:";
2063
+ var ThreadHistoryCache = class {
2064
+ state;
2065
+ maxMessages;
2066
+ ttlMs;
2067
+ constructor(state, config) {
2068
+ this.state = state;
2069
+ this.maxMessages = config?.maxMessages ?? DEFAULT_MAX_MESSAGES;
2070
+ this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
2071
+ }
2072
+ /**
2073
+ * Atomically append a message to the history for a thread.
2074
+ * Trims to maxMessages (keeps newest) and refreshes TTL.
2075
+ */
2076
+ async append(threadId, message) {
2077
+ const key = `${KEY_PREFIX}${threadId}`;
2078
+ const serialized = message.toJSON();
2079
+ serialized.raw = null;
2080
+ await this.state.appendToList(key, serialized, {
2081
+ maxLength: this.maxMessages,
2082
+ ttlMs: this.ttlMs
2083
+ });
2084
+ }
2085
+ /**
2086
+ * Get messages for a thread in chronological order (oldest first).
2087
+ *
2088
+ * @param threadId - The thread ID
2089
+ * @param limit - Optional limit on number of messages to return (returns newest N)
2090
+ */
2091
+ async getMessages(threadId, limit) {
2092
+ const key = `${KEY_PREFIX}${threadId}`;
2093
+ const stored = await this.state.getList(key);
2094
+ const sliced = limit && stored.length > limit ? stored.slice(stored.length - limit) : stored;
2095
+ return sliced.map((s) => Message.fromJSON(s));
2096
+ }
2097
+ };
2098
+
2099
+ // src/transcripts.ts
2100
+ var KEY_PREFIX2 = "transcripts:user:";
2101
+ var DEFAULT_MAX_PER_USER = 200;
2102
+ var DEFAULT_LIST_LIMIT = 50;
2103
+ var DURATION_RE = /^(\d+)([smhd])$/;
2104
+ var TOMBSTONE_MARKER = "__chatSdkTombstone";
2105
+ function isTombstone(value) {
2106
+ return typeof value === "object" && value !== null && value[TOMBSTONE_MARKER] === true;
2107
+ }
2108
+ var MS_PER_UNIT = {
2109
+ s: 1e3,
2110
+ m: 6e4,
2111
+ h: 36e5,
2112
+ d: 864e5
2113
+ };
2114
+ var TranscriptsApiImpl = class {
2115
+ state;
2116
+ maxPerUser;
2117
+ retentionMs;
2118
+ storeFormatted;
2119
+ constructor(state, config) {
2120
+ this.state = state;
2121
+ this.maxPerUser = config.maxPerUser ?? DEFAULT_MAX_PER_USER;
2122
+ this.retentionMs = parseDuration(config.retention);
2123
+ this.storeFormatted = config.storeFormatted ?? false;
2124
+ }
2125
+ async append(thread, message, options) {
2126
+ const isMessage = message instanceof Message;
2127
+ let userKey;
2128
+ let role;
2129
+ let platformMessageId;
2130
+ if (isMessage) {
2131
+ userKey = message.userKey;
2132
+ role = "user";
2133
+ platformMessageId = message.id;
2134
+ if (!userKey) {
2135
+ return null;
2136
+ }
2137
+ } else {
2138
+ userKey = options?.userKey;
2139
+ role = message.role;
2140
+ platformMessageId = message.platformMessageId;
2141
+ if (!userKey) {
2142
+ throw new Error(
2143
+ "transcripts.append: options.userKey is required when appending an AppendInput"
2144
+ );
2145
+ }
2146
+ }
2147
+ const entry = {
2148
+ id: crypto.randomUUID(),
2149
+ userKey,
2150
+ role,
2151
+ text: message.text,
2152
+ platform: thread.adapter.name,
2153
+ threadId: thread.id,
2154
+ timestamp: Date.now()
2155
+ };
2156
+ if (this.storeFormatted && message.formatted) {
2157
+ entry.formatted = message.formatted;
2158
+ }
2159
+ if (platformMessageId !== void 0) {
2160
+ entry.platformMessageId = platformMessageId;
2161
+ }
2162
+ await this.state.appendToList(keyFor(userKey), entry, {
2163
+ maxLength: this.maxPerUser,
2164
+ ttlMs: this.retentionMs
2165
+ });
2166
+ return entry;
2167
+ }
2168
+ async list(query) {
2169
+ const raw = await this.state.getList(
2170
+ keyFor(query.userKey)
2171
+ );
2172
+ let filtered = raw.filter(
2173
+ (entry) => !isTombstone(entry)
2174
+ );
2175
+ if (query.platforms && query.platforms.length > 0) {
2176
+ const platforms = new Set(query.platforms);
2177
+ filtered = filtered.filter((m) => platforms.has(m.platform));
2178
+ }
2179
+ if (query.threadId !== void 0) {
2180
+ const tid = query.threadId;
2181
+ filtered = filtered.filter((m) => m.threadId === tid);
2182
+ }
2183
+ if (query.roles && query.roles.length > 0) {
2184
+ const roles = new Set(query.roles);
2185
+ filtered = filtered.filter((m) => roles.has(m.role));
2186
+ }
2187
+ const limit = query.limit ?? DEFAULT_LIST_LIMIT;
2188
+ if (filtered.length > limit) {
2189
+ filtered = filtered.slice(filtered.length - limit);
2190
+ }
2191
+ return filtered;
2192
+ }
2193
+ async count(query) {
2194
+ const raw = await this.state.getList(keyFor(query.userKey));
2195
+ return raw.filter((entry) => !isTombstone(entry)).length;
2196
+ }
2197
+ async delete(target) {
2198
+ const key = keyFor(target.userKey);
2199
+ const existing = await this.state.getList(key);
2200
+ const previous = existing.filter((entry) => !isTombstone(entry)).length;
2201
+ const tombstone = { [TOMBSTONE_MARKER]: true };
2202
+ await this.state.appendToList(key, tombstone, {
2203
+ maxLength: 1,
2204
+ ttlMs: this.retentionMs
2205
+ });
2206
+ return { deleted: previous };
2207
+ }
2208
+ };
2209
+ function keyFor(userKey) {
2210
+ return `${KEY_PREFIX2}${userKey}`;
2211
+ }
2212
+ function parseDuration(value) {
2213
+ if (value === void 0) {
2214
+ return void 0;
2215
+ }
2216
+ if (typeof value === "number") {
2217
+ return value;
2218
+ }
2219
+ const match = DURATION_RE.exec(value);
2220
+ if (!match) {
2221
+ throw new Error(
2222
+ `Invalid duration: ${value} (expected number of ms, or "<n>[smhd]")`
2223
+ );
2224
+ }
2225
+ const n = Number.parseInt(match[1], 10);
2226
+ const unit = match[2];
2227
+ return n * MS_PER_UNIT[unit];
2228
+ }
2229
+
1854
2230
  // src/chat.ts
1855
2231
  var DEFAULT_LOCK_TTL_MS = 3e4;
1856
2232
  function sleep(ms) {
1857
2233
  return new Promise((resolve) => setTimeout(resolve, ms));
1858
2234
  }
1859
- var SLACK_USER_ID_REGEX = /^U[A-Z0-9]+$/i;
2235
+ var SLACK_USER_ID_REGEX = /^[UW][A-Z0-9]+$/;
1860
2236
  var DISCORD_SNOWFLAKE_REGEX = /^\d{17,19}$/;
2237
+ var LINEAR_UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2238
+ var NUMERIC_REGEX = /^\d+$/;
1861
2239
  var DEDUPE_TTL_MS = 5 * 60 * 1e3;
1862
2240
  var MODAL_CONTEXT_TTL_MS = 24 * 60 * 60 * 1e3;
1863
2241
  var Chat = class {
@@ -1878,6 +2256,21 @@ var Chat = class {
1878
2256
  setChatSingleton(this);
1879
2257
  return this;
1880
2258
  }
2259
+ /**
2260
+ * Cross-platform per-user transcript store.
2261
+ *
2262
+ * Available only when `transcripts` is configured on the Chat instance
2263
+ * (and an `identity` resolver is set). Throws on access otherwise so
2264
+ * callers fail loudly rather than silently no-op'ing.
2265
+ */
2266
+ get transcripts() {
2267
+ if (!this._transcripts) {
2268
+ throw new Error(
2269
+ "chat.transcripts is not configured \u2014 pass `transcripts` and `identity` to ChatConfig to enable it"
2270
+ );
2271
+ }
2272
+ return this._transcripts;
2273
+ }
1881
2274
  /**
1882
2275
  * Get the registered singleton Chat instance.
1883
2276
  * Throws if no singleton has been registered.
@@ -1899,9 +2292,12 @@ var Chat = class {
1899
2292
  _fallbackStreamingPlaceholderText;
1900
2293
  _dedupeTtlMs;
1901
2294
  _onLockConflict;
1902
- _messageHistory;
2295
+ _threadHistory;
2296
+ _identity;
2297
+ _transcripts;
1903
2298
  _concurrencyStrategy;
1904
2299
  _concurrencyConfig;
2300
+ _concurrentSlots = /* @__PURE__ */ new Map();
1905
2301
  _lockScope;
1906
2302
  mentionHandlers = [];
1907
2303
  directMessageHandlers = [];
@@ -1909,6 +2305,7 @@ var Chat = class {
1909
2305
  subscribedMessageHandlers = [];
1910
2306
  reactionHandlers = [];
1911
2307
  actionHandlers = [];
2308
+ optionsLoadHandlers = [];
1912
2309
  modalSubmitHandlers = [];
1913
2310
  modalCloseHandlers = [];
1914
2311
  slashCommandHandlers = [];
@@ -1934,6 +2331,11 @@ var Chat = class {
1934
2331
  this._dedupeTtlMs = config.dedupeTtlMs ?? DEDUPE_TTL_MS;
1935
2332
  this._onLockConflict = config.onLockConflict;
1936
2333
  this._lockScope = config.lockScope;
2334
+ if (typeof config.logger === "string") {
2335
+ this.logger = new ConsoleLogger(config.logger);
2336
+ } else {
2337
+ this.logger = config.logger || new ConsoleLogger("info");
2338
+ }
1937
2339
  const concurrency = config.concurrency;
1938
2340
  if (concurrency) {
1939
2341
  if (typeof concurrency === "string") {
@@ -1946,6 +2348,16 @@ var Chat = class {
1946
2348
  queueEntryTtlMs: 9e4
1947
2349
  };
1948
2350
  } else {
2351
+ if (concurrency.maxConcurrent !== void 0 && concurrency.maxConcurrent < 1) {
2352
+ throw new Error(
2353
+ `concurrency.maxConcurrent must be >= 1 (got ${concurrency.maxConcurrent})`
2354
+ );
2355
+ }
2356
+ if (concurrency.maxConcurrent !== void 0 && concurrency.strategy !== "concurrent") {
2357
+ this.logger.warn(
2358
+ `concurrency.maxConcurrent has no effect when strategy is "${concurrency.strategy}" \u2014 it only applies to the "concurrent" strategy.`
2359
+ );
2360
+ }
1949
2361
  this._concurrencyStrategy = concurrency.strategy;
1950
2362
  this._concurrencyConfig = {
1951
2363
  debounceMs: concurrency.debounceMs ?? 1500,
@@ -1965,14 +2377,23 @@ var Chat = class {
1965
2377
  queueEntryTtlMs: 9e4
1966
2378
  };
1967
2379
  }
1968
- this._messageHistory = new MessageHistoryCache(
2380
+ this._threadHistory = new ThreadHistoryCache(
1969
2381
  this._stateAdapter,
1970
- config.messageHistory
2382
+ config.threadHistory ?? config.messageHistory
1971
2383
  );
1972
- if (typeof config.logger === "string") {
1973
- this.logger = new ConsoleLogger(config.logger);
2384
+ if (config.transcripts) {
2385
+ if (!config.identity) {
2386
+ throw new Error(
2387
+ "ChatConfig.transcripts requires ChatConfig.identity to be set \u2014 the cross-platform user key must be resolvable"
2388
+ );
2389
+ }
2390
+ this._identity = config.identity;
2391
+ this._transcripts = new TranscriptsApiImpl(
2392
+ this._stateAdapter,
2393
+ config.transcripts
2394
+ );
1974
2395
  } else {
1975
- this.logger = config.logger || new ConsoleLogger("info");
2396
+ this._identity = config.identity;
1976
2397
  }
1977
2398
  const webhooks = {};
1978
2399
  for (const [name, adapter] of Object.entries(config.adapters)) {
@@ -2180,6 +2601,19 @@ var Chat = class {
2180
2601
  this.logger.debug("Registered action handler", { actionIds });
2181
2602
  }
2182
2603
  }
2604
+ onOptionsLoad(actionIdOrHandler, handler) {
2605
+ if (typeof actionIdOrHandler === "function") {
2606
+ this.optionsLoadHandlers.push({
2607
+ actionIds: [],
2608
+ handler: actionIdOrHandler
2609
+ });
2610
+ this.logger.debug("Registered options load handler for all action IDs");
2611
+ } else if (handler) {
2612
+ const actionIds = Array.isArray(actionIdOrHandler) ? actionIdOrHandler : [actionIdOrHandler];
2613
+ this.optionsLoadHandlers.push({ actionIds, handler });
2614
+ this.logger.debug("Registered options load handler", { actionIds });
2615
+ }
2616
+ }
2183
2617
  onModalSubmit(callbackIdOrHandler, handler) {
2184
2618
  if (typeof callbackIdOrHandler === "function") {
2185
2619
  this.modalSubmitHandlers.push({
@@ -2279,12 +2713,14 @@ var Chat = class {
2279
2713
  const task = (async () => {
2280
2714
  const message = typeof messageOrFactory === "function" ? await messageOrFactory() : messageOrFactory;
2281
2715
  await this.handleIncomingMessage(adapter, threadId, message);
2282
- })().catch((err) => {
2716
+ })();
2717
+ const tracked = task.catch((err) => {
2283
2718
  this.logger.error("Message processing error", { error: err, threadId });
2284
2719
  });
2285
2720
  if (options?.waitUntil) {
2286
- options.waitUntil(task);
2721
+ options.waitUntil(tracked);
2287
2722
  }
2723
+ return task;
2288
2724
  }
2289
2725
  /**
2290
2726
  * Process an incoming reaction event from an adapter.
@@ -2319,20 +2755,45 @@ var Chat = class {
2319
2755
  }
2320
2756
  return task;
2321
2757
  }
2322
- async processModalSubmit(event, contextId, _options) {
2323
- const { relatedThread, relatedMessage, relatedChannel } = await this.retrieveModalContext(event.adapter.name, contextId);
2758
+ async processOptionsLoad(event, _options) {
2759
+ const matchingHandlers = [
2760
+ ...this.optionsLoadHandlers.filter(
2761
+ ({ actionIds }) => actionIds.length > 0 && actionIds.includes(event.actionId)
2762
+ ),
2763
+ ...this.optionsLoadHandlers.filter(
2764
+ ({ actionIds }) => actionIds.length === 0
2765
+ )
2766
+ ];
2767
+ for (const { handler } of matchingHandlers) {
2768
+ try {
2769
+ const options = await handler(event);
2770
+ if (options) {
2771
+ return options;
2772
+ }
2773
+ } catch (err) {
2774
+ this.logger.error("Options load handler error", {
2775
+ error: err,
2776
+ actionId: event.actionId
2777
+ });
2778
+ }
2779
+ }
2780
+ }
2781
+ async processModalSubmit(event, contextId, options) {
2782
+ const { callbackUrl, relatedThread, relatedMessage, relatedChannel } = await this.retrieveModalContext(event.adapter.name, contextId);
2324
2783
  const fullEvent = {
2325
2784
  ...event,
2326
2785
  relatedThread,
2327
2786
  relatedMessage,
2328
2787
  relatedChannel
2329
2788
  };
2789
+ let result;
2330
2790
  for (const { callbackIds, handler } of this.modalSubmitHandlers) {
2331
2791
  if (callbackIds.length === 0 || callbackIds.includes(event.callbackId)) {
2332
2792
  try {
2333
2793
  const response = await handler(fullEvent);
2334
2794
  if (response) {
2335
- return response;
2795
+ result = response;
2796
+ break;
2336
2797
  }
2337
2798
  } catch (err) {
2338
2799
  this.logger.error("Modal submit handler error", {
@@ -2342,6 +2803,30 @@ var Chat = class {
2342
2803
  }
2343
2804
  }
2344
2805
  }
2806
+ if (callbackUrl && result?.action !== "errors") {
2807
+ const task = postToCallbackUrl(callbackUrl, {
2808
+ type: "modal_submit",
2809
+ callbackId: event.callbackId,
2810
+ values: event.values,
2811
+ user: { id: event.user.userId, name: event.user.userName }
2812
+ }).then(({ error }) => {
2813
+ if (error) {
2814
+ this.logger.error("Modal callbackUrl POST failed", {
2815
+ callbackUrl,
2816
+ error
2817
+ });
2818
+ }
2819
+ }).catch((error) => {
2820
+ this.logger.error("Modal callbackUrl POST failed", {
2821
+ callbackUrl,
2822
+ error
2823
+ });
2824
+ });
2825
+ if (options?.waitUntil) {
2826
+ options.waitUntil(task);
2827
+ }
2828
+ }
2829
+ return result;
2345
2830
  }
2346
2831
  processModalClose(event, contextId, options) {
2347
2832
  const task = (async () => {
@@ -2493,7 +2978,8 @@ var Chat = class {
2493
2978
  contextId,
2494
2979
  void 0,
2495
2980
  void 0,
2496
- channel
2981
+ channel,
2982
+ modalElement.callbackUrl
2497
2983
  );
2498
2984
  if (options?.onOpenModal) {
2499
2985
  return options.onOpenModal(modalElement, contextId);
@@ -2530,12 +3016,13 @@ var Chat = class {
2530
3016
  * Store modal context server-side with a context ID.
2531
3017
  * Called when opening a modal to preserve thread/message/channel for the submit handler.
2532
3018
  */
2533
- async storeModalContext(adapterName, contextId, thread, message, channel) {
3019
+ async storeModalContext(adapterName, contextId, thread, message, channel, callbackUrl) {
2534
3020
  const key = `modal-context:${adapterName}:${contextId}`;
2535
3021
  const context = {
2536
3022
  thread: thread?.toJSON(),
2537
3023
  message: message?.toJSON(),
2538
- channel: channel?.toJSON()
3024
+ channel: channel?.toJSON(),
3025
+ callbackUrl
2539
3026
  };
2540
3027
  try {
2541
3028
  await this._stateAdapter.set(key, context, MODAL_CONTEXT_TTL_MS);
@@ -2553,6 +3040,7 @@ var Chat = class {
2553
3040
  async retrieveModalContext(adapterName, contextId) {
2554
3041
  if (!contextId) {
2555
3042
  return {
3043
+ callbackUrl: void 0,
2556
3044
  relatedThread: void 0,
2557
3045
  relatedMessage: void 0,
2558
3046
  relatedChannel: void 0
@@ -2562,6 +3050,7 @@ var Chat = class {
2562
3050
  const stored = await this._stateAdapter.get(key);
2563
3051
  if (!stored) {
2564
3052
  return {
3053
+ callbackUrl: void 0,
2565
3054
  relatedThread: void 0,
2566
3055
  relatedMessage: void 0,
2567
3056
  relatedChannel: void 0
@@ -2582,7 +3071,12 @@ var Chat = class {
2582
3071
  if (stored.channel) {
2583
3072
  relatedChannel = ChannelImpl.fromJSON(stored.channel, adapter);
2584
3073
  }
2585
- return { relatedThread, relatedMessage, relatedChannel };
3074
+ return {
3075
+ callbackUrl: stored.callbackUrl,
3076
+ relatedThread,
3077
+ relatedMessage,
3078
+ relatedChannel
3079
+ };
2586
3080
  }
2587
3081
  /**
2588
3082
  * Handle an action event internally.
@@ -2602,6 +3096,33 @@ var Chat = class {
2602
3096
  });
2603
3097
  return;
2604
3098
  }
3099
+ const { callbackToken } = decodeCallbackValue(event.value);
3100
+ let resolved = null;
3101
+ if (callbackToken) {
3102
+ resolved = await resolveCallbackUrl(callbackToken, this._stateAdapter);
3103
+ }
3104
+ const actionEvent = resolved ? { ...event, value: resolved.originalValue } : event;
3105
+ let callbackUrlPromise;
3106
+ if (resolved) {
3107
+ const callbackUrl = resolved.url;
3108
+ callbackUrlPromise = (async () => {
3109
+ const { error } = await postToCallbackUrl(callbackUrl, {
3110
+ type: "action",
3111
+ actionId: event.actionId,
3112
+ value: resolved.originalValue,
3113
+ user: { id: event.user.userId, name: event.user.userName },
3114
+ threadId: event.threadId,
3115
+ messageId: event.messageId
3116
+ });
3117
+ if (error) {
3118
+ this.logger.error("Button callbackUrl POST failed", {
3119
+ callbackUrl,
3120
+ actionId: event.actionId,
3121
+ error
3122
+ });
3123
+ }
3124
+ })();
3125
+ }
2605
3126
  const isSubscribed = false;
2606
3127
  const messageForThread = event.messageId ? new Message({
2607
3128
  id: event.messageId,
@@ -2620,7 +3141,7 @@ var Chat = class {
2620
3141
  isSubscribed
2621
3142
  ) : null;
2622
3143
  const fullEvent = {
2623
- ...event,
3144
+ ...actionEvent,
2624
3145
  thread,
2625
3146
  openModal: async (modal) => {
2626
3147
  if (!(event.triggerId || options?.onOpenModal)) {
@@ -2668,7 +3189,8 @@ var Chat = class {
2668
3189
  contextId,
2669
3190
  thread ? thread : void 0,
2670
3191
  message,
2671
- channel
3192
+ channel,
3193
+ modalElement.callbackUrl
2672
3194
  );
2673
3195
  if (options?.onOpenModal) {
2674
3196
  return options.onOpenModal(modalElement, contextId);
@@ -2700,6 +3222,9 @@ var Chat = class {
2700
3222
  await handler(fullEvent);
2701
3223
  }
2702
3224
  }
3225
+ if (callbackUrlPromise) {
3226
+ await callbackUrlPromise;
3227
+ }
2703
3228
  }
2704
3229
  /**
2705
3230
  * Handle a reaction event internally.
@@ -2818,6 +3343,33 @@ var Chat = class {
2818
3343
  const threadId = await adapter.openDM(userId);
2819
3344
  return this.createThread(adapter, threadId, {}, false);
2820
3345
  }
3346
+ /**
3347
+ * Look up user information by user ID.
3348
+ *
3349
+ * The adapter is automatically inferred from the user ID format.
3350
+ * Returns user details including email (where available — requires
3351
+ * appropriate scopes on some platforms, e.g. `users:read.email` on Slack).
3352
+ *
3353
+ * @param user - Platform-specific user ID string, or an Author object
3354
+ * @returns User info, or null if user not found
3355
+ *
3356
+ * @example
3357
+ * ```typescript
3358
+ * const user = await chat.getUser("U123456");
3359
+ * console.log(user?.email); // "alice@company.com"
3360
+ * ```
3361
+ */
3362
+ async getUser(user) {
3363
+ const userId = typeof user === "string" ? user : user.userId;
3364
+ const adapter = this.inferAdapterFromUserId(userId);
3365
+ if (!adapter.getUser) {
3366
+ throw new ChatError(
3367
+ `Adapter "${adapter.name}" does not support getUser`,
3368
+ "NOT_SUPPORTED"
3369
+ );
3370
+ }
3371
+ return adapter.getUser(userId);
3372
+ }
2821
3373
  /**
2822
3374
  * Get a Channel by its channel ID.
2823
3375
  *
@@ -2865,6 +3417,37 @@ var Chat = class {
2865
3417
  stateAdapter: this._stateAdapter
2866
3418
  });
2867
3419
  }
3420
+ /**
3421
+ * Get a Thread handle by its thread ID.
3422
+ *
3423
+ * The adapter is automatically inferred from the thread ID prefix.
3424
+ *
3425
+ * @param threadId - Full thread ID (e.g., "slack:C123ABC:1234567890.123456")
3426
+ * @returns A Thread that can be used to post messages, subscribe, etc.
3427
+ *
3428
+ * @example
3429
+ * ```typescript
3430
+ * const thread = chat.thread("slack:C123ABC:1234567890.123456");
3431
+ * await thread.post("Hello from outside a webhook!");
3432
+ * ```
3433
+ */
3434
+ thread(threadId) {
3435
+ const adapterName = threadId.split(":")[0];
3436
+ if (!adapterName) {
3437
+ throw new ChatError(
3438
+ `Invalid thread ID: ${threadId}`,
3439
+ "INVALID_THREAD_ID"
3440
+ );
3441
+ }
3442
+ const adapter = this.adapters.get(adapterName);
3443
+ if (!adapter) {
3444
+ throw new ChatError(
3445
+ `Adapter "${adapterName}" not found for thread ID "${threadId}"`,
3446
+ "ADAPTER_NOT_FOUND"
3447
+ );
3448
+ }
3449
+ return this.createThread(adapter, threadId, {}, false);
3450
+ }
2868
3451
  /**
2869
3452
  * Infer which adapter to use based on the userId format.
2870
3453
  */
@@ -2881,20 +3464,44 @@ var Chat = class {
2881
3464
  return adapter;
2882
3465
  }
2883
3466
  }
2884
- if (SLACK_USER_ID_REGEX.test(userId)) {
2885
- const adapter = this.adapters.get("slack");
3467
+ if (LINEAR_UUID_REGEX.test(userId)) {
3468
+ const adapter = this.adapters.get("linear");
2886
3469
  if (adapter) {
2887
3470
  return adapter;
2888
3471
  }
2889
3472
  }
2890
- if (DISCORD_SNOWFLAKE_REGEX.test(userId)) {
2891
- const adapter = this.adapters.get("discord");
3473
+ if (SLACK_USER_ID_REGEX.test(userId)) {
3474
+ const adapter = this.adapters.get("slack");
2892
3475
  if (adapter) {
2893
3476
  return adapter;
2894
3477
  }
2895
3478
  }
3479
+ if (NUMERIC_REGEX.test(userId)) {
3480
+ const candidates = [];
3481
+ if (DISCORD_SNOWFLAKE_REGEX.test(userId) && this.adapters.has("discord")) {
3482
+ candidates.push("discord");
3483
+ }
3484
+ if (this.adapters.has("telegram")) {
3485
+ candidates.push("telegram");
3486
+ }
3487
+ if (this.adapters.has("github")) {
3488
+ candidates.push("github");
3489
+ }
3490
+ if (candidates.length === 1) {
3491
+ const adapter = this.adapters.get(candidates[0]);
3492
+ if (adapter) {
3493
+ return adapter;
3494
+ }
3495
+ }
3496
+ if (candidates.length > 1) {
3497
+ throw new ChatError(
3498
+ `Numeric userId "${userId}" is ambiguous between adapters: ${candidates.join(", ")}. Call the platform's adapter directly (e.g. \`adapter.getUser(userId)\`).`,
3499
+ "AMBIGUOUS_USER_ID"
3500
+ );
3501
+ }
3502
+ }
2896
3503
  throw new ChatError(
2897
- `Cannot infer adapter from userId "${userId}". Expected format: Slack (U...), Teams (29:...), Google Chat (users/...), or Discord (numeric snowflake).`,
3504
+ `Cannot infer adapter from userId "${userId}". Expected: Slack ("U..."), Teams ("29:..."), Google Chat ("users/..."), Linear (UUID), or Discord/Telegram/GitHub (numeric).`,
2898
3505
  "UNKNOWN_USER_ID_FORMAT"
2899
3506
  );
2900
3507
  }
@@ -2930,6 +3537,7 @@ var Chat = class {
2930
3537
  * - Concurrency: Controlled by `concurrency` config (drop, queue, debounce, concurrent)
2931
3538
  */
2932
3539
  async handleIncomingMessage(adapter, threadId, message) {
3540
+ setMessageAdapter(message, adapter);
2933
3541
  this.logger.debug("Incoming message", {
2934
3542
  adapter: adapter.name,
2935
3543
  threadId,
@@ -2961,11 +3569,11 @@ var Chat = class {
2961
3569
  });
2962
3570
  return;
2963
3571
  }
2964
- if (adapter.persistMessageHistory) {
3572
+ if (adapter.persistThreadHistory || adapter.persistMessageHistory) {
2965
3573
  const channelId = adapter.channelIdFromThreadId(threadId);
2966
- const appends = [this._messageHistory.append(threadId, message)];
3574
+ const appends = [this._threadHistory.append(threadId, message)];
2967
3575
  if (channelId !== threadId) {
2968
- appends.push(this._messageHistory.append(channelId, message));
3576
+ appends.push(this._threadHistory.append(channelId, message));
2969
3577
  }
2970
3578
  await Promise.all(appends);
2971
3579
  }
@@ -3116,7 +3724,7 @@ var Chat = class {
3116
3724
  if (!entry) {
3117
3725
  break;
3118
3726
  }
3119
- const msg = this.rehydrateMessage(entry.message);
3727
+ const msg = this.rehydrateMessage(entry.message, adapter);
3120
3728
  if (Date.now() > entry.expiresAt) {
3121
3729
  this.logger.info("message-expired", {
3122
3730
  threadId,
@@ -3155,7 +3763,7 @@ var Chat = class {
3155
3763
  if (!entry) {
3156
3764
  break;
3157
3765
  }
3158
- const msg = this.rehydrateMessage(entry.message);
3766
+ const msg = this.rehydrateMessage(entry.message, adapter);
3159
3767
  if (Date.now() <= entry.expiresAt) {
3160
3768
  pending.push({ message: msg, expiresAt: entry.expiresAt });
3161
3769
  } else {
@@ -3190,10 +3798,50 @@ var Chat = class {
3190
3798
  }
3191
3799
  }
3192
3800
  /**
3193
- * Concurrent strategy: no locking, process immediately.
3801
+ * Concurrent strategy: no locking, process immediately — but cap
3802
+ * simultaneous handlers per thread at `maxConcurrent` (default Infinity).
3194
3803
  */
3195
3804
  async handleConcurrent(adapter, threadId, message) {
3196
- await this.dispatchToHandlers(adapter, threadId, message);
3805
+ const { maxConcurrent } = this._concurrencyConfig;
3806
+ if (!Number.isFinite(maxConcurrent)) {
3807
+ await this.dispatchToHandlers(adapter, threadId, message);
3808
+ return;
3809
+ }
3810
+ await this.acquireConcurrentSlot(threadId, maxConcurrent);
3811
+ try {
3812
+ await this.dispatchToHandlers(adapter, threadId, message);
3813
+ } finally {
3814
+ this.releaseConcurrentSlot(threadId);
3815
+ }
3816
+ }
3817
+ acquireConcurrentSlot(threadId, maxConcurrent) {
3818
+ let slot = this._concurrentSlots.get(threadId);
3819
+ if (!slot) {
3820
+ slot = { inFlight: 0, waiters: [] };
3821
+ this._concurrentSlots.set(threadId, slot);
3822
+ }
3823
+ if (slot.inFlight < maxConcurrent) {
3824
+ slot.inFlight++;
3825
+ return Promise.resolve();
3826
+ }
3827
+ return new Promise((resolve) => {
3828
+ slot.waiters.push(resolve);
3829
+ });
3830
+ }
3831
+ releaseConcurrentSlot(threadId) {
3832
+ const slot = this._concurrentSlots.get(threadId);
3833
+ if (!slot) {
3834
+ return;
3835
+ }
3836
+ const next = slot.waiters.shift();
3837
+ if (next) {
3838
+ next();
3839
+ return;
3840
+ }
3841
+ slot.inFlight--;
3842
+ if (slot.inFlight === 0 && slot.waiters.length === 0) {
3843
+ this._concurrentSlots.delete(threadId);
3844
+ }
3197
3845
  }
3198
3846
  /**
3199
3847
  * Dispatch a message to the appropriate handler chain based on
@@ -3213,6 +3861,25 @@ var Chat = class {
3213
3861
  message,
3214
3862
  isSubscribed
3215
3863
  );
3864
+ if (this._identity && message.userKey === void 0) {
3865
+ try {
3866
+ const resolved = await this._identity({
3867
+ adapter: adapter.name,
3868
+ author: message.author,
3869
+ message
3870
+ });
3871
+ if (resolved) {
3872
+ message.userKey = resolved;
3873
+ }
3874
+ } catch (err) {
3875
+ this.logger.warn("Identity resolver threw; skipping userKey", {
3876
+ error: err,
3877
+ adapter: adapter.name,
3878
+ threadId,
3879
+ authorUserId: message.author.userId
3880
+ });
3881
+ }
3882
+ }
3216
3883
  const isDM = adapter.isDM?.(threadId) ?? false;
3217
3884
  if (isDM && this.directMessageHandlers.length > 0) {
3218
3885
  this.logger.debug("Direct message received - calling handlers", {
@@ -3294,7 +3961,7 @@ var Chat = class {
3294
3961
  logger: this.logger,
3295
3962
  streamingUpdateIntervalMs: this._streamingUpdateIntervalMs,
3296
3963
  fallbackStreamingPlaceholderText: this._fallbackStreamingPlaceholderText,
3297
- messageHistory: adapter.persistMessageHistory ? this._messageHistory : void 0
3964
+ threadHistory: adapter.persistThreadHistory || adapter.persistMessageHistory ? this._threadHistory : void 0
3298
3965
  });
3299
3966
  }
3300
3967
  /**
@@ -3338,35 +4005,50 @@ var Chat = class {
3338
4005
  * object (not a Message instance). This restores class invariants like
3339
4006
  * `links` defaulting to `[]` and `metadata.dateSent` being a Date.
3340
4007
  */
3341
- rehydrateMessage(raw) {
4008
+ rehydrateMessage(raw, adapter) {
3342
4009
  if (raw instanceof Message) {
4010
+ if (adapter) {
4011
+ setMessageAdapter(raw, adapter);
4012
+ }
3343
4013
  return raw;
3344
4014
  }
3345
4015
  const obj = raw;
4016
+ let msg;
3346
4017
  if (obj._type === "chat:Message") {
3347
- return Message.fromJSON(obj);
3348
- }
3349
- const metadata = obj.metadata;
3350
- const dateSent = metadata.dateSent;
3351
- const editedAt = metadata.editedAt;
3352
- return new Message({
3353
- id: obj.id,
3354
- threadId: obj.threadId,
3355
- text: obj.text,
3356
- formatted: obj.formatted,
3357
- raw: obj.raw,
3358
- author: obj.author,
3359
- metadata: {
3360
- dateSent: dateSent instanceof Date ? dateSent : new Date(dateSent),
3361
- edited: metadata.edited,
3362
- editedAt: editedAt ? new Date(
3363
- editedAt instanceof Date ? editedAt.toISOString() : editedAt
3364
- ) : void 0
3365
- },
3366
- attachments: obj.attachments ?? [],
3367
- isMention: obj.isMention,
3368
- links: obj.links ?? []
3369
- });
4018
+ msg = Message.fromJSON(obj);
4019
+ } else {
4020
+ const metadata = obj.metadata;
4021
+ const dateSent = metadata.dateSent;
4022
+ const editedAt = metadata.editedAt;
4023
+ msg = new Message({
4024
+ id: obj.id,
4025
+ threadId: obj.threadId,
4026
+ text: obj.text,
4027
+ formatted: obj.formatted,
4028
+ raw: obj.raw,
4029
+ author: obj.author,
4030
+ metadata: {
4031
+ dateSent: dateSent instanceof Date ? dateSent : new Date(dateSent),
4032
+ edited: metadata.edited,
4033
+ editedAt: editedAt ? new Date(
4034
+ editedAt instanceof Date ? editedAt.toISOString() : editedAt
4035
+ ) : void 0
4036
+ },
4037
+ attachments: obj.attachments ?? [],
4038
+ isMention: obj.isMention,
4039
+ links: obj.links ?? []
4040
+ });
4041
+ }
4042
+ if (adapter) {
4043
+ setMessageAdapter(msg, adapter);
4044
+ }
4045
+ const rehydrate = adapter?.rehydrateAttachment?.bind(adapter);
4046
+ if (rehydrate && msg.attachments.length > 0) {
4047
+ msg.attachments = msg.attachments.map(
4048
+ (att) => att.fetchData ? att : rehydrate(att)
4049
+ );
4050
+ }
4051
+ return msg;
3370
4052
  }
3371
4053
  async runHandlers(handlers, thread, message, context) {
3372
4054
  for (const handler of handlers) {
@@ -3375,6 +4057,9 @@ var Chat = class {
3375
4057
  }
3376
4058
  };
3377
4059
 
4060
+ // src/message-history.ts
4061
+ var MessageHistoryCache = ThreadHistoryCache;
4062
+
3378
4063
  // src/plan.ts
3379
4064
  function contentToPlainText(content) {
3380
4065
  if (!content) {
@@ -3494,13 +4179,17 @@ var Plan = class {
3494
4179
  return null;
3495
4180
  }
3496
4181
  let current;
3497
- for (let i = this._model.tasks.length - 1; i >= 0; i--) {
3498
- if (this._model.tasks[i].status === "in_progress") {
3499
- current = this._model.tasks[i];
3500
- break;
4182
+ if (typeof update === "object" && update !== null && "id" in update && update.id) {
4183
+ current = this._model.tasks.find((t) => t.id === update.id);
4184
+ } else {
4185
+ for (let i = this._model.tasks.length - 1; i >= 0; i--) {
4186
+ if (this._model.tasks[i].status === "in_progress") {
4187
+ current = this._model.tasks[i];
4188
+ break;
4189
+ }
3501
4190
  }
4191
+ current ??= this._model.tasks.at(-1);
3502
4192
  }
3503
- current ??= this._model.tasks.at(-1);
3504
4193
  if (!current) {
3505
4194
  return null;
3506
4195
  }
@@ -3589,6 +4278,38 @@ var Plan = class {
3589
4278
  }
3590
4279
  };
3591
4280
 
4281
+ // src/streaming-plan.ts
4282
+ var StreamingPlan = class {
4283
+ $$typeof = POSTABLE_OBJECT;
4284
+ kind = "stream";
4285
+ _stream;
4286
+ _options;
4287
+ constructor(stream, options = {}) {
4288
+ this._stream = stream;
4289
+ this._options = options;
4290
+ }
4291
+ get stream() {
4292
+ return this._stream;
4293
+ }
4294
+ get options() {
4295
+ return this._options;
4296
+ }
4297
+ getFallbackText() {
4298
+ return "";
4299
+ }
4300
+ getPostData() {
4301
+ return {
4302
+ stream: this._stream,
4303
+ options: this._options
4304
+ };
4305
+ }
4306
+ isSupported(_adapter) {
4307
+ return true;
4308
+ }
4309
+ onPosted(_context) {
4310
+ }
4311
+ };
4312
+
3592
4313
  // src/emoji.ts
3593
4314
  var emojiRegistry = /* @__PURE__ */ new Map();
3594
4315
  function getEmoji(name) {
@@ -3839,6 +4560,8 @@ function convertEmojiPlaceholders(text2, platform, resolver = defaultEmojiResolv
3839
4560
  return resolver.toGChat(emojiName);
3840
4561
  case "discord":
3841
4562
  return resolver.toDiscord(emojiName);
4563
+ case "messenger":
4564
+ return resolver.toGChat(emojiName);
3842
4565
  case "github":
3843
4566
  return resolver.toGChat(emojiName);
3844
4567
  case "linear":
@@ -3999,6 +4722,7 @@ var toCardElement2 = toCardElement;
3999
4722
  var toModalElement2 = toModalElement;
4000
4723
  var fromReactModalElement2 = fromReactModalElement;
4001
4724
  var isModalElement2 = isModalElement;
4725
+ var ExternalSelect2 = ExternalSelect;
4002
4726
  var Modal2 = Modal;
4003
4727
  var RadioSelect2 = RadioSelect;
4004
4728
  var Select2 = Select;
@@ -4018,6 +4742,7 @@ export {
4018
4742
  DEFAULT_EMOJI_MAP,
4019
4743
  Divider2 as Divider,
4020
4744
  EmojiResolver,
4745
+ ExternalSelect2 as ExternalSelect,
4021
4746
  Field2 as Field,
4022
4747
  Fields2 as Fields,
4023
4748
  Image2 as Image,
@@ -4034,9 +4759,11 @@ export {
4034
4759
  Select2 as Select,
4035
4760
  SelectOption2 as SelectOption,
4036
4761
  StreamingMarkdownRenderer,
4762
+ StreamingPlan,
4037
4763
  THREAD_STATE_TTL_MS,
4038
4764
  Table2 as Table,
4039
4765
  TextInput2 as TextInput,
4766
+ ThreadHistoryCache,
4040
4767
  ThreadImpl,
4041
4768
  blockquote,
4042
4769
  cardChildToFallbackText2 as cardChildToFallbackText,