chat 4.19.0 → 4.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -59,7 +59,132 @@ import {
59
59
  toModalElement,
60
60
  toPlainText,
61
61
  walkAst
62
- } from "./chunk-WAB7KMH4.js";
62
+ } from "./chunk-JW7GYSMH.js";
63
+
64
+ // src/ai.ts
65
+ var TEXT_MIME_PREFIXES = [
66
+ "text/",
67
+ "application/json",
68
+ "application/xml",
69
+ "application/javascript",
70
+ "application/typescript",
71
+ "application/yaml",
72
+ "application/x-yaml",
73
+ "application/toml"
74
+ ];
75
+ function isTextMimeType(mimeType) {
76
+ return TEXT_MIME_PREFIXES.some(
77
+ (prefix) => mimeType === prefix || mimeType.startsWith(prefix)
78
+ );
79
+ }
80
+ async function attachmentToPart(att) {
81
+ if (att.type === "image") {
82
+ if (att.fetchData) {
83
+ try {
84
+ const buffer = await att.fetchData();
85
+ const mimeType = att.mimeType ?? "image/png";
86
+ return {
87
+ type: "file",
88
+ data: `data:${mimeType};base64,${buffer.toString("base64")}`,
89
+ mediaType: mimeType,
90
+ filename: att.name
91
+ };
92
+ } catch (error) {
93
+ console.error("toAiMessages: failed to fetch image data", error);
94
+ return null;
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ if (att.type === "file" && att.mimeType && isTextMimeType(att.mimeType)) {
100
+ if (att.fetchData) {
101
+ try {
102
+ const buffer = await att.fetchData();
103
+ return {
104
+ type: "file",
105
+ data: `data:${att.mimeType};base64,${buffer.toString("base64")}`,
106
+ filename: att.name,
107
+ mediaType: att.mimeType
108
+ };
109
+ } catch (error) {
110
+ console.error("toAiMessages: failed to fetch file data", error);
111
+ return null;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ return null;
117
+ }
118
+ async function toAiMessages(messages, options) {
119
+ const includeNames = options?.includeNames ?? false;
120
+ const transformMessage = options?.transformMessage;
121
+ const onUnsupported = options?.onUnsupportedAttachment ?? ((att) => {
122
+ console.warn(
123
+ `toAiMessages: unsupported attachment type "${att.type}"${att.name ? ` (${att.name})` : ""} \u2014 skipped`
124
+ );
125
+ });
126
+ const sorted = [...messages].sort(
127
+ (a, b) => (a.metadata.dateSent?.getTime() ?? 0) - (b.metadata.dateSent?.getTime() ?? 0)
128
+ );
129
+ const filtered = sorted.filter((msg) => msg.text.trim());
130
+ const results = await Promise.all(
131
+ filtered.map(async (msg) => {
132
+ const role = msg.author.isMe ? "assistant" : "user";
133
+ let textContent = includeNames && role === "user" ? `[${msg.author.userName}]: ${msg.text}` : msg.text;
134
+ if (msg.links.length > 0) {
135
+ const linkParts = msg.links.map((link2) => {
136
+ const parts = link2.fetchMessage ? [`[Embedded message: ${link2.url}]`] : [link2.url];
137
+ if (link2.title) {
138
+ parts.push(`Title: ${link2.title}`);
139
+ }
140
+ if (link2.description) {
141
+ parts.push(`Description: ${link2.description}`);
142
+ }
143
+ if (link2.siteName) {
144
+ parts.push(`Site: ${link2.siteName}`);
145
+ }
146
+ return parts.join("\n");
147
+ }).join("\n\n");
148
+ textContent += `
149
+
150
+ Links:
151
+ ${linkParts}`;
152
+ }
153
+ let aiMessage;
154
+ if (role === "user") {
155
+ const attachmentParts = [];
156
+ for (const att of msg.attachments) {
157
+ const part = await attachmentToPart(att);
158
+ if (part) {
159
+ attachmentParts.push(part);
160
+ } else if (att.type === "video" || att.type === "audio") {
161
+ onUnsupported(att, msg);
162
+ }
163
+ }
164
+ if (attachmentParts.length > 0) {
165
+ aiMessage = {
166
+ role,
167
+ content: [
168
+ { type: "text", text: textContent },
169
+ ...attachmentParts
170
+ ]
171
+ };
172
+ } else {
173
+ aiMessage = { role, content: textContent };
174
+ }
175
+ } else {
176
+ aiMessage = { role, content: textContent };
177
+ }
178
+ if (transformMessage) {
179
+ return { result: await transformMessage(aiMessage, msg), source: msg };
180
+ }
181
+ return { result: aiMessage, source: msg };
182
+ })
183
+ );
184
+ return results.filter(
185
+ (r) => r.result != null
186
+ ).map((r) => r.result);
187
+ }
63
188
 
64
189
  // src/channel.ts
65
190
  import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE2, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE2 } from "@workflow/serde";
@@ -81,6 +206,42 @@ function hasChatSingleton() {
81
206
  return _singleton !== null;
82
207
  }
83
208
 
209
+ // src/from-full-stream.ts
210
+ var STREAM_CHUNK_TYPES = /* @__PURE__ */ new Set([
211
+ "markdown_text",
212
+ "task_update",
213
+ "plan_update"
214
+ ]);
215
+ async function* fromFullStream(stream) {
216
+ let needsSeparator = false;
217
+ let hasEmittedText = false;
218
+ for await (const event of stream) {
219
+ if (typeof event === "string") {
220
+ yield event;
221
+ continue;
222
+ }
223
+ if (event === null || typeof event !== "object" || !("type" in event)) {
224
+ continue;
225
+ }
226
+ const typed = event;
227
+ if (STREAM_CHUNK_TYPES.has(typed.type)) {
228
+ yield event;
229
+ continue;
230
+ }
231
+ const textContent = typed.text ?? typed.delta ?? typed.textDelta;
232
+ if (typed.type === "text-delta" && typeof textContent === "string") {
233
+ if (needsSeparator && hasEmittedText) {
234
+ yield "\n\n";
235
+ }
236
+ needsSeparator = false;
237
+ hasEmittedText = true;
238
+ yield textContent;
239
+ } else if (typed.type === "step-finish") {
240
+ needsSeparator = true;
241
+ }
242
+ }
243
+ }
244
+
84
245
  // src/message.ts
85
246
  import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";
86
247
  var Message = class _Message {
@@ -121,6 +282,8 @@ var Message = class _Message {
121
282
  * ```
122
283
  */
123
284
  isMention;
285
+ /** Links found in the message */
286
+ links;
124
287
  constructor(data) {
125
288
  this.id = data.id;
126
289
  this.threadId = data.threadId;
@@ -131,6 +294,7 @@ var Message = class _Message {
131
294
  this.metadata = data.metadata;
132
295
  this.attachments = data.attachments;
133
296
  this.isMention = data.isMention;
297
+ this.links = data.links ?? [];
134
298
  }
135
299
  /**
136
300
  * Serialize the message to a plain JSON object.
@@ -168,7 +332,14 @@ var Message = class _Message {
168
332
  width: att.width,
169
333
  height: att.height
170
334
  })),
171
- isMention: this.isMention
335
+ isMention: this.isMention,
336
+ links: this.links.length > 0 ? this.links.map((link2) => ({
337
+ url: link2.url,
338
+ title: link2.title,
339
+ description: link2.description,
340
+ imageUrl: link2.imageUrl,
341
+ siteName: link2.siteName
342
+ })) : void 0
172
343
  };
173
344
  }
174
345
  /**
@@ -189,7 +360,8 @@ var Message = class _Message {
189
360
  editedAt: json.metadata.editedAt ? new Date(json.metadata.editedAt) : void 0
190
361
  },
191
362
  attachments: json.attachments,
192
- isMention: json.isMention
363
+ isMention: json.isMention,
364
+ links: json.links
193
365
  });
194
366
  }
195
367
  /**
@@ -301,6 +473,7 @@ var ChannelImpl = class _ChannelImpl {
301
473
  _adapterName;
302
474
  _stateAdapterInstance;
303
475
  _name = null;
476
+ _messageHistory;
304
477
  constructor(config) {
305
478
  this.id = config.id;
306
479
  this.isDM = config.isDM ?? false;
@@ -309,6 +482,7 @@ var ChannelImpl = class _ChannelImpl {
309
482
  } else {
310
483
  this._adapter = config.adapter;
311
484
  this._stateAdapterInstance = config.stateAdapter;
485
+ this._messageHistory = config.messageHistory;
312
486
  }
313
487
  }
314
488
  get adapter() {
@@ -362,14 +536,17 @@ var ChannelImpl = class _ChannelImpl {
362
536
  get messages() {
363
537
  const adapter = this.adapter;
364
538
  const channelId = this.id;
539
+ const messageHistory = this._messageHistory;
365
540
  return {
366
541
  async *[Symbol.asyncIterator]() {
367
542
  let cursor;
543
+ let yieldedAny = false;
368
544
  while (true) {
369
545
  const fetchOptions = { cursor, direction: "backward" };
370
546
  const result = adapter.fetchChannelMessages ? await adapter.fetchChannelMessages(channelId, fetchOptions) : await adapter.fetchMessages(channelId, fetchOptions);
371
547
  const reversed = [...result.messages].reverse();
372
548
  for (const message of reversed) {
549
+ yieldedAny = true;
373
550
  yield message;
374
551
  }
375
552
  if (!result.nextCursor || result.messages.length === 0) {
@@ -377,6 +554,12 @@ var ChannelImpl = class _ChannelImpl {
377
554
  }
378
555
  cursor = result.nextCursor;
379
556
  }
557
+ if (!yieldedAny && messageHistory) {
558
+ const cached = await messageHistory.getMessages(channelId);
559
+ for (let i = cached.length - 1; i >= 0; i--) {
560
+ yield cached[i];
561
+ }
562
+ }
380
563
  }
381
564
  };
382
565
  }
@@ -422,10 +605,12 @@ var ChannelImpl = class _ChannelImpl {
422
605
  async post(message) {
423
606
  if (isAsyncIterable(message)) {
424
607
  let accumulated = "";
425
- for await (const chunk of message) {
426
- accumulated += chunk;
608
+ for await (const chunk of fromFullStream(message)) {
609
+ if (typeof chunk === "string") {
610
+ accumulated += chunk;
611
+ }
427
612
  }
428
- return this.postSingleMessage(accumulated);
613
+ return this.postSingleMessage({ markdown: accumulated });
429
614
  }
430
615
  let postable = message;
431
616
  if (isJSX(message)) {
@@ -439,7 +624,15 @@ var ChannelImpl = class _ChannelImpl {
439
624
  }
440
625
  async postSingleMessage(postable) {
441
626
  const rawMessage = this.adapter.postChannelMessage ? await this.adapter.postChannelMessage(this.id, postable) : await this.adapter.postMessage(this.id, postable);
442
- return this.createSentMessage(rawMessage.id, postable, rawMessage.threadId);
627
+ const sent = this.createSentMessage(
628
+ rawMessage.id,
629
+ postable,
630
+ rawMessage.threadId
631
+ );
632
+ if (this._messageHistory) {
633
+ await this._messageHistory.append(this.id, new Message(sent));
634
+ }
635
+ return sent;
443
636
  }
444
637
  async postEphemeral(user, message, options) {
445
638
  const { fallbackToDM } = options;
@@ -545,6 +738,7 @@ var ChannelImpl = class _ChannelImpl {
545
738
  edited: false
546
739
  },
547
740
  attachments,
741
+ links: [],
548
742
  toJSON() {
549
743
  return new Message(this).toJSON();
550
744
  },
@@ -625,45 +819,49 @@ function extractMessageContent(message) {
625
819
  throw new Error("Invalid PostableMessage format");
626
820
  }
627
821
 
822
+ // src/message-history.ts
823
+ var DEFAULT_MAX_MESSAGES = 100;
824
+ var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
825
+ var KEY_PREFIX = "msg-history:";
826
+ var MessageHistoryCache = class {
827
+ state;
828
+ maxMessages;
829
+ ttlMs;
830
+ constructor(state, config) {
831
+ this.state = state;
832
+ this.maxMessages = config?.maxMessages ?? DEFAULT_MAX_MESSAGES;
833
+ this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
834
+ }
835
+ /**
836
+ * Atomically append a message to the history for a thread.
837
+ * Trims to maxMessages (keeps newest) and refreshes TTL.
838
+ */
839
+ async append(threadId, message) {
840
+ const key = `${KEY_PREFIX}${threadId}`;
841
+ const serialized = message.toJSON();
842
+ serialized.raw = null;
843
+ await this.state.appendToList(key, serialized, {
844
+ maxLength: this.maxMessages,
845
+ ttlMs: this.ttlMs
846
+ });
847
+ }
848
+ /**
849
+ * Get messages for a thread in chronological order (oldest first).
850
+ *
851
+ * @param threadId - The thread ID
852
+ * @param limit - Optional limit on number of messages to return (returns newest N)
853
+ */
854
+ async getMessages(threadId, limit) {
855
+ const key = `${KEY_PREFIX}${threadId}`;
856
+ const stored = await this.state.getList(key);
857
+ const sliced = limit && stored.length > limit ? stored.slice(stored.length - limit) : stored;
858
+ return sliced.map((s) => Message.fromJSON(s));
859
+ }
860
+ };
861
+
628
862
  // src/thread.ts
629
863
  import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE3, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE3 } from "@workflow/serde";
630
864
 
631
- // src/from-full-stream.ts
632
- var STREAM_CHUNK_TYPES = /* @__PURE__ */ new Set([
633
- "markdown_text",
634
- "task_update",
635
- "plan_update"
636
- ]);
637
- async function* fromFullStream(stream) {
638
- let needsSeparator = false;
639
- let hasEmittedText = false;
640
- for await (const event of stream) {
641
- if (typeof event === "string") {
642
- yield event;
643
- continue;
644
- }
645
- if (event === null || typeof event !== "object" || !("type" in event)) {
646
- continue;
647
- }
648
- const typed = event;
649
- if (STREAM_CHUNK_TYPES.has(typed.type)) {
650
- yield event;
651
- continue;
652
- }
653
- const textContent = typed.text ?? typed.delta ?? typed.textDelta;
654
- if (typed.type === "text-delta" && typeof textContent === "string") {
655
- if (needsSeparator && hasEmittedText) {
656
- yield "\n\n";
657
- }
658
- needsSeparator = false;
659
- hasEmittedText = true;
660
- yield textContent;
661
- } else if (typed.type === "step-finish") {
662
- needsSeparator = true;
663
- }
664
- }
665
- }
666
-
667
865
  // src/streaming-markdown.ts
668
866
  import remend from "remend";
669
867
  var StreamingMarkdownRenderer = class {
@@ -917,12 +1115,16 @@ var ThreadImpl = class _ThreadImpl {
917
1115
  _fallbackStreamingPlaceholderText;
918
1116
  /** Cached channel instance */
919
1117
  _channel;
1118
+ /** Message history cache (set only for adapters with persistMessageHistory) */
1119
+ _messageHistory;
1120
+ _logger;
920
1121
  constructor(config) {
921
1122
  this.id = config.id;
922
1123
  this.channelId = config.channelId;
923
1124
  this.isDM = config.isDM ?? false;
924
1125
  this._isSubscribedContext = config.isSubscribedContext ?? false;
925
1126
  this._currentMessage = config.currentMessage;
1127
+ this._logger = config.logger;
926
1128
  this._streamingUpdateIntervalMs = config.streamingUpdateIntervalMs ?? 500;
927
1129
  this._fallbackStreamingPlaceholderText = config.fallbackStreamingPlaceholderText !== void 0 ? config.fallbackStreamingPlaceholderText : "...";
928
1130
  if (isLazyConfig2(config)) {
@@ -930,6 +1132,7 @@ var ThreadImpl = class _ThreadImpl {
930
1132
  } else {
931
1133
  this._adapter = config.adapter;
932
1134
  this._stateAdapterInstance = config.stateAdapter;
1135
+ this._messageHistory = config.messageHistory;
933
1136
  }
934
1137
  if (config.initialMessage) {
935
1138
  this._recentMessages = [config.initialMessage];
@@ -1008,7 +1211,8 @@ var ThreadImpl = class _ThreadImpl {
1008
1211
  id: channelId,
1009
1212
  adapter: this.adapter,
1010
1213
  stateAdapter: this._stateAdapter,
1011
- isDM: this.isDM
1214
+ isDM: this.isDM,
1215
+ messageHistory: this._messageHistory
1012
1216
  });
1013
1217
  }
1014
1218
  return this._channel;
@@ -1020,9 +1224,11 @@ var ThreadImpl = class _ThreadImpl {
1020
1224
  get messages() {
1021
1225
  const adapter = this.adapter;
1022
1226
  const threadId = this.id;
1227
+ const messageHistory = this._messageHistory;
1023
1228
  return {
1024
1229
  async *[Symbol.asyncIterator]() {
1025
1230
  let cursor;
1231
+ let yieldedAny = false;
1026
1232
  while (true) {
1027
1233
  const result = await adapter.fetchMessages(threadId, {
1028
1234
  cursor,
@@ -1030,6 +1236,7 @@ var ThreadImpl = class _ThreadImpl {
1030
1236
  });
1031
1237
  const reversed = [...result.messages].reverse();
1032
1238
  for (const message of reversed) {
1239
+ yieldedAny = true;
1033
1240
  yield message;
1034
1241
  }
1035
1242
  if (!result.nextCursor || result.messages.length === 0) {
@@ -1037,15 +1244,23 @@ var ThreadImpl = class _ThreadImpl {
1037
1244
  }
1038
1245
  cursor = result.nextCursor;
1039
1246
  }
1247
+ if (!yieldedAny && messageHistory) {
1248
+ const cached = await messageHistory.getMessages(threadId);
1249
+ for (let i = cached.length - 1; i >= 0; i--) {
1250
+ yield cached[i];
1251
+ }
1252
+ }
1040
1253
  }
1041
1254
  };
1042
1255
  }
1043
1256
  get allMessages() {
1044
1257
  const adapter = this.adapter;
1045
1258
  const threadId = this.id;
1259
+ const messageHistory = this._messageHistory;
1046
1260
  return {
1047
1261
  async *[Symbol.asyncIterator]() {
1048
1262
  let cursor;
1263
+ let yieldedAny = false;
1049
1264
  while (true) {
1050
1265
  const result = await adapter.fetchMessages(threadId, {
1051
1266
  limit: 100,
@@ -1053,6 +1268,7 @@ var ThreadImpl = class _ThreadImpl {
1053
1268
  direction: "forward"
1054
1269
  });
1055
1270
  for (const message of result.messages) {
1271
+ yieldedAny = true;
1056
1272
  yield message;
1057
1273
  }
1058
1274
  if (!result.nextCursor || result.messages.length === 0) {
@@ -1060,6 +1276,12 @@ var ThreadImpl = class _ThreadImpl {
1060
1276
  }
1061
1277
  cursor = result.nextCursor;
1062
1278
  }
1279
+ if (!yieldedAny && messageHistory) {
1280
+ const cached = await messageHistory.getMessages(threadId);
1281
+ for (const message of cached) {
1282
+ yield message;
1283
+ }
1284
+ }
1063
1285
  }
1064
1286
  };
1065
1287
  }
@@ -1096,6 +1318,9 @@ var ThreadImpl = class _ThreadImpl {
1096
1318
  postable,
1097
1319
  rawMessage.threadId
1098
1320
  );
1321
+ if (this._messageHistory) {
1322
+ await this._messageHistory.append(this.id, new Message(result));
1323
+ }
1099
1324
  return result;
1100
1325
  }
1101
1326
  async postEphemeral(user, message, options) {
@@ -1183,11 +1408,15 @@ var ThreadImpl = class _ThreadImpl {
1183
1408
  }
1184
1409
  };
1185
1410
  const raw = await this.adapter.stream(this.id, wrappedStream, options);
1186
- return this.createSentMessage(
1411
+ const sent = this.createSentMessage(
1187
1412
  raw.id,
1188
1413
  { markdown: accumulated },
1189
1414
  raw.threadId
1190
1415
  );
1416
+ if (this._messageHistory) {
1417
+ await this._messageHistory.append(this.id, new Message(sent));
1418
+ }
1419
+ return sent;
1191
1420
  }
1192
1421
  const textOnlyStream = {
1193
1422
  [Symbol.asyncIterator]: () => {
@@ -1252,7 +1481,8 @@ var ThreadImpl = class _ThreadImpl {
1252
1481
  markdown: content
1253
1482
  });
1254
1483
  lastEditContent = content;
1255
- } catch {
1484
+ } catch (error) {
1485
+ this._logger?.warn("fallbackStream edit failed", error);
1256
1486
  }
1257
1487
  }
1258
1488
  if (!stopped) {
@@ -1299,15 +1529,28 @@ var ThreadImpl = class _ThreadImpl {
1299
1529
  markdown: accumulated
1300
1530
  });
1301
1531
  }
1302
- return this.createSentMessage(
1532
+ const sent = this.createSentMessage(
1303
1533
  msg.id,
1304
1534
  { markdown: accumulated },
1305
1535
  threadIdForEdits
1306
1536
  );
1537
+ if (this._messageHistory) {
1538
+ await this._messageHistory.append(this.id, new Message(sent));
1539
+ }
1540
+ return sent;
1307
1541
  }
1308
1542
  async refresh() {
1309
1543
  const result = await this.adapter.fetchMessages(this.id, { limit: 50 });
1310
- this._recentMessages = result.messages;
1544
+ if (result.messages.length > 0) {
1545
+ this._recentMessages = result.messages;
1546
+ } else if (this._messageHistory) {
1547
+ this._recentMessages = await this._messageHistory.getMessages(
1548
+ this.id,
1549
+ 50
1550
+ );
1551
+ } else {
1552
+ this._recentMessages = [];
1553
+ }
1311
1554
  }
1312
1555
  mentionUser(userId) {
1313
1556
  return `<@${userId}>`;
@@ -1389,6 +1632,7 @@ var ThreadImpl = class _ThreadImpl {
1389
1632
  formatted,
1390
1633
  raw: null,
1391
1634
  // Will be populated if needed
1635
+ links: [],
1392
1636
  author: {
1393
1637
  userId: "self",
1394
1638
  userName: adapter.userName,
@@ -1442,6 +1686,7 @@ var ThreadImpl = class _ThreadImpl {
1442
1686
  author: message.author,
1443
1687
  metadata: message.metadata,
1444
1688
  attachments: message.attachments,
1689
+ links: message.links,
1445
1690
  isMention: message.isMention,
1446
1691
  toJSON() {
1447
1692
  return message.toJSON();
@@ -1564,7 +1809,9 @@ var Chat = class {
1564
1809
  _fallbackStreamingPlaceholderText;
1565
1810
  _dedupeTtlMs;
1566
1811
  _onLockConflict;
1812
+ _messageHistory;
1567
1813
  mentionHandlers = [];
1814
+ directMessageHandlers = [];
1568
1815
  messagePatterns = [];
1569
1816
  subscribedMessageHandlers = [];
1570
1817
  reactionHandlers = [];
@@ -1593,6 +1840,10 @@ var Chat = class {
1593
1840
  this._fallbackStreamingPlaceholderText = config.fallbackStreamingPlaceholderText !== void 0 ? config.fallbackStreamingPlaceholderText : "...";
1594
1841
  this._dedupeTtlMs = config.dedupeTtlMs ?? DEDUPE_TTL_MS;
1595
1842
  this._onLockConflict = config.onLockConflict;
1843
+ this._messageHistory = new MessageHistoryCache(
1844
+ this._stateAdapter,
1845
+ config.messageHistory
1846
+ );
1596
1847
  if (typeof config.logger === "string") {
1597
1848
  this.logger = new ConsoleLogger(config.logger);
1598
1849
  } else {
@@ -1699,6 +1950,27 @@ var Chat = class {
1699
1950
  this.mentionHandlers.push(handler);
1700
1951
  this.logger.debug("Registered mention handler");
1701
1952
  }
1953
+ /**
1954
+ * Register a handler for direct messages.
1955
+ *
1956
+ * Called when a message is received in a DM thread that is not subscribed.
1957
+ * If no `onDirectMessage` handlers are registered, DMs fall through to
1958
+ * `onNewMention` for backward compatibility.
1959
+ *
1960
+ * @param handler - Handler called for DM messages
1961
+ *
1962
+ * @example
1963
+ * ```typescript
1964
+ * chat.onDirectMessage(async (thread, message) => {
1965
+ * await thread.subscribe();
1966
+ * await thread.post("Thanks for the DM!");
1967
+ * });
1968
+ * ```
1969
+ */
1970
+ onDirectMessage(handler) {
1971
+ this.directMessageHandlers.push(handler);
1972
+ this.logger.debug("Registered direct message handler");
1973
+ }
1702
1974
  /**
1703
1975
  * Register a handler for messages matching a regex pattern.
1704
1976
  *
@@ -2525,6 +2797,14 @@ var Chat = class {
2525
2797
  });
2526
2798
  return;
2527
2799
  }
2800
+ if (adapter.persistMessageHistory) {
2801
+ const channelId = adapter.channelIdFromThreadId(threadId);
2802
+ const appends = [this._messageHistory.append(threadId, message)];
2803
+ if (channelId !== threadId) {
2804
+ appends.push(this._messageHistory.append(channelId, message));
2805
+ }
2806
+ await Promise.all(appends);
2807
+ }
2528
2808
  let lock = await this._stateAdapter.acquireLock(
2529
2809
  threadId,
2530
2810
  DEFAULT_LOCK_TTL_MS
@@ -2561,6 +2841,21 @@ var Chat = class {
2561
2841
  message,
2562
2842
  isSubscribed
2563
2843
  );
2844
+ const isDM = adapter.isDM?.(threadId) ?? false;
2845
+ if (isDM && this.directMessageHandlers.length > 0) {
2846
+ this.logger.debug("Direct message received - calling handlers", {
2847
+ threadId,
2848
+ handlerCount: this.directMessageHandlers.length
2849
+ });
2850
+ const channel = thread.channel;
2851
+ for (const handler of this.directMessageHandlers) {
2852
+ await handler(thread, message, channel);
2853
+ }
2854
+ return;
2855
+ }
2856
+ if (isDM) {
2857
+ message.isMention = true;
2858
+ }
2564
2859
  if (isSubscribed) {
2565
2860
  this.logger.debug("Message in subscribed thread - calling handlers", {
2566
2861
  threadId,
@@ -2622,8 +2917,10 @@ var Chat = class {
2622
2917
  isSubscribedContext,
2623
2918
  isDM,
2624
2919
  currentMessage: initialMessage,
2920
+ logger: this.logger,
2625
2921
  streamingUpdateIntervalMs: this._streamingUpdateIntervalMs,
2626
- fallbackStreamingPlaceholderText: this._fallbackStreamingPlaceholderText
2922
+ fallbackStreamingPlaceholderText: this._fallbackStreamingPlaceholderText,
2923
+ messageHistory: adapter.persistMessageHistory ? this._messageHistory : void 0
2627
2924
  });
2628
2925
  }
2629
2926
  /**
@@ -2922,6 +3219,8 @@ function convertEmojiPlaceholders(text2, platform, resolver = defaultEmojiResolv
2922
3219
  return resolver.toGChat(emojiName);
2923
3220
  case "linear":
2924
3221
  return resolver.toGChat(emojiName);
3222
+ case "whatsapp":
3223
+ return resolver.toGChat(emojiName);
2925
3224
  default:
2926
3225
  return resolver.toGChat(emojiName);
2927
3226
  }
@@ -3101,6 +3400,7 @@ export {
3101
3400
  LinkButton2 as LinkButton,
3102
3401
  LockError,
3103
3402
  Message,
3403
+ MessageHistoryCache,
3104
3404
  Modal2 as Modal,
3105
3405
  NotImplementedError,
3106
3406
  RadioSelect2 as RadioSelect,
@@ -3157,6 +3457,7 @@ export {
3157
3457
  tableElementToAscii,
3158
3458
  tableToAscii,
3159
3459
  text,
3460
+ toAiMessages,
3160
3461
  toCardElement2 as toCardElement,
3161
3462
  toModalElement2 as toModalElement,
3162
3463
  toPlainText,