chat 4.19.0 → 4.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -59,7 +59,20 @@ 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
+ function toAiMessages(messages, options) {
66
+ const includeNames = options?.includeNames ?? false;
67
+ const sorted = [...messages].sort(
68
+ (a, b) => (a.metadata.dateSent?.getTime() ?? 0) - (b.metadata.dateSent?.getTime() ?? 0)
69
+ );
70
+ return sorted.filter((msg) => msg.text.trim()).map((msg) => {
71
+ const role = msg.author.isMe ? "assistant" : "user";
72
+ const content = includeNames && role === "user" ? `[${msg.author.userName}]: ${msg.text}` : msg.text;
73
+ return { role, content };
74
+ });
75
+ }
63
76
 
64
77
  // src/channel.ts
65
78
  import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE2, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE2 } from "@workflow/serde";
@@ -81,6 +94,42 @@ function hasChatSingleton() {
81
94
  return _singleton !== null;
82
95
  }
83
96
 
97
+ // src/from-full-stream.ts
98
+ var STREAM_CHUNK_TYPES = /* @__PURE__ */ new Set([
99
+ "markdown_text",
100
+ "task_update",
101
+ "plan_update"
102
+ ]);
103
+ async function* fromFullStream(stream) {
104
+ let needsSeparator = false;
105
+ let hasEmittedText = false;
106
+ for await (const event of stream) {
107
+ if (typeof event === "string") {
108
+ yield event;
109
+ continue;
110
+ }
111
+ if (event === null || typeof event !== "object" || !("type" in event)) {
112
+ continue;
113
+ }
114
+ const typed = event;
115
+ if (STREAM_CHUNK_TYPES.has(typed.type)) {
116
+ yield event;
117
+ continue;
118
+ }
119
+ const textContent = typed.text ?? typed.delta ?? typed.textDelta;
120
+ if (typed.type === "text-delta" && typeof textContent === "string") {
121
+ if (needsSeparator && hasEmittedText) {
122
+ yield "\n\n";
123
+ }
124
+ needsSeparator = false;
125
+ hasEmittedText = true;
126
+ yield textContent;
127
+ } else if (typed.type === "step-finish") {
128
+ needsSeparator = true;
129
+ }
130
+ }
131
+ }
132
+
84
133
  // src/message.ts
85
134
  import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";
86
135
  var Message = class _Message {
@@ -301,6 +350,7 @@ var ChannelImpl = class _ChannelImpl {
301
350
  _adapterName;
302
351
  _stateAdapterInstance;
303
352
  _name = null;
353
+ _messageHistory;
304
354
  constructor(config) {
305
355
  this.id = config.id;
306
356
  this.isDM = config.isDM ?? false;
@@ -309,6 +359,7 @@ var ChannelImpl = class _ChannelImpl {
309
359
  } else {
310
360
  this._adapter = config.adapter;
311
361
  this._stateAdapterInstance = config.stateAdapter;
362
+ this._messageHistory = config.messageHistory;
312
363
  }
313
364
  }
314
365
  get adapter() {
@@ -362,14 +413,17 @@ var ChannelImpl = class _ChannelImpl {
362
413
  get messages() {
363
414
  const adapter = this.adapter;
364
415
  const channelId = this.id;
416
+ const messageHistory = this._messageHistory;
365
417
  return {
366
418
  async *[Symbol.asyncIterator]() {
367
419
  let cursor;
420
+ let yieldedAny = false;
368
421
  while (true) {
369
422
  const fetchOptions = { cursor, direction: "backward" };
370
423
  const result = adapter.fetchChannelMessages ? await adapter.fetchChannelMessages(channelId, fetchOptions) : await adapter.fetchMessages(channelId, fetchOptions);
371
424
  const reversed = [...result.messages].reverse();
372
425
  for (const message of reversed) {
426
+ yieldedAny = true;
373
427
  yield message;
374
428
  }
375
429
  if (!result.nextCursor || result.messages.length === 0) {
@@ -377,6 +431,12 @@ var ChannelImpl = class _ChannelImpl {
377
431
  }
378
432
  cursor = result.nextCursor;
379
433
  }
434
+ if (!yieldedAny && messageHistory) {
435
+ const cached = await messageHistory.getMessages(channelId);
436
+ for (let i = cached.length - 1; i >= 0; i--) {
437
+ yield cached[i];
438
+ }
439
+ }
380
440
  }
381
441
  };
382
442
  }
@@ -422,10 +482,12 @@ var ChannelImpl = class _ChannelImpl {
422
482
  async post(message) {
423
483
  if (isAsyncIterable(message)) {
424
484
  let accumulated = "";
425
- for await (const chunk of message) {
426
- accumulated += chunk;
485
+ for await (const chunk of fromFullStream(message)) {
486
+ if (typeof chunk === "string") {
487
+ accumulated += chunk;
488
+ }
427
489
  }
428
- return this.postSingleMessage(accumulated);
490
+ return this.postSingleMessage({ markdown: accumulated });
429
491
  }
430
492
  let postable = message;
431
493
  if (isJSX(message)) {
@@ -439,7 +501,15 @@ var ChannelImpl = class _ChannelImpl {
439
501
  }
440
502
  async postSingleMessage(postable) {
441
503
  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);
504
+ const sent = this.createSentMessage(
505
+ rawMessage.id,
506
+ postable,
507
+ rawMessage.threadId
508
+ );
509
+ if (this._messageHistory) {
510
+ await this._messageHistory.append(this.id, new Message(sent));
511
+ }
512
+ return sent;
443
513
  }
444
514
  async postEphemeral(user, message, options) {
445
515
  const { fallbackToDM } = options;
@@ -625,45 +695,49 @@ function extractMessageContent(message) {
625
695
  throw new Error("Invalid PostableMessage format");
626
696
  }
627
697
 
698
+ // src/message-history.ts
699
+ var DEFAULT_MAX_MESSAGES = 100;
700
+ var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
701
+ var KEY_PREFIX = "msg-history:";
702
+ var MessageHistoryCache = class {
703
+ state;
704
+ maxMessages;
705
+ ttlMs;
706
+ constructor(state, config) {
707
+ this.state = state;
708
+ this.maxMessages = config?.maxMessages ?? DEFAULT_MAX_MESSAGES;
709
+ this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
710
+ }
711
+ /**
712
+ * Atomically append a message to the history for a thread.
713
+ * Trims to maxMessages (keeps newest) and refreshes TTL.
714
+ */
715
+ async append(threadId, message) {
716
+ const key = `${KEY_PREFIX}${threadId}`;
717
+ const serialized = message.toJSON();
718
+ serialized.raw = null;
719
+ await this.state.appendToList(key, serialized, {
720
+ maxLength: this.maxMessages,
721
+ ttlMs: this.ttlMs
722
+ });
723
+ }
724
+ /**
725
+ * Get messages for a thread in chronological order (oldest first).
726
+ *
727
+ * @param threadId - The thread ID
728
+ * @param limit - Optional limit on number of messages to return (returns newest N)
729
+ */
730
+ async getMessages(threadId, limit) {
731
+ const key = `${KEY_PREFIX}${threadId}`;
732
+ const stored = await this.state.getList(key);
733
+ const sliced = limit && stored.length > limit ? stored.slice(stored.length - limit) : stored;
734
+ return sliced.map((s) => Message.fromJSON(s));
735
+ }
736
+ };
737
+
628
738
  // src/thread.ts
629
739
  import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE3, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE3 } from "@workflow/serde";
630
740
 
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
741
  // src/streaming-markdown.ts
668
742
  import remend from "remend";
669
743
  var StreamingMarkdownRenderer = class {
@@ -917,6 +991,8 @@ var ThreadImpl = class _ThreadImpl {
917
991
  _fallbackStreamingPlaceholderText;
918
992
  /** Cached channel instance */
919
993
  _channel;
994
+ /** Message history cache (set only for adapters with persistMessageHistory) */
995
+ _messageHistory;
920
996
  constructor(config) {
921
997
  this.id = config.id;
922
998
  this.channelId = config.channelId;
@@ -930,6 +1006,7 @@ var ThreadImpl = class _ThreadImpl {
930
1006
  } else {
931
1007
  this._adapter = config.adapter;
932
1008
  this._stateAdapterInstance = config.stateAdapter;
1009
+ this._messageHistory = config.messageHistory;
933
1010
  }
934
1011
  if (config.initialMessage) {
935
1012
  this._recentMessages = [config.initialMessage];
@@ -1008,7 +1085,8 @@ var ThreadImpl = class _ThreadImpl {
1008
1085
  id: channelId,
1009
1086
  adapter: this.adapter,
1010
1087
  stateAdapter: this._stateAdapter,
1011
- isDM: this.isDM
1088
+ isDM: this.isDM,
1089
+ messageHistory: this._messageHistory
1012
1090
  });
1013
1091
  }
1014
1092
  return this._channel;
@@ -1020,9 +1098,11 @@ var ThreadImpl = class _ThreadImpl {
1020
1098
  get messages() {
1021
1099
  const adapter = this.adapter;
1022
1100
  const threadId = this.id;
1101
+ const messageHistory = this._messageHistory;
1023
1102
  return {
1024
1103
  async *[Symbol.asyncIterator]() {
1025
1104
  let cursor;
1105
+ let yieldedAny = false;
1026
1106
  while (true) {
1027
1107
  const result = await adapter.fetchMessages(threadId, {
1028
1108
  cursor,
@@ -1030,6 +1110,7 @@ var ThreadImpl = class _ThreadImpl {
1030
1110
  });
1031
1111
  const reversed = [...result.messages].reverse();
1032
1112
  for (const message of reversed) {
1113
+ yieldedAny = true;
1033
1114
  yield message;
1034
1115
  }
1035
1116
  if (!result.nextCursor || result.messages.length === 0) {
@@ -1037,15 +1118,23 @@ var ThreadImpl = class _ThreadImpl {
1037
1118
  }
1038
1119
  cursor = result.nextCursor;
1039
1120
  }
1121
+ if (!yieldedAny && messageHistory) {
1122
+ const cached = await messageHistory.getMessages(threadId);
1123
+ for (let i = cached.length - 1; i >= 0; i--) {
1124
+ yield cached[i];
1125
+ }
1126
+ }
1040
1127
  }
1041
1128
  };
1042
1129
  }
1043
1130
  get allMessages() {
1044
1131
  const adapter = this.adapter;
1045
1132
  const threadId = this.id;
1133
+ const messageHistory = this._messageHistory;
1046
1134
  return {
1047
1135
  async *[Symbol.asyncIterator]() {
1048
1136
  let cursor;
1137
+ let yieldedAny = false;
1049
1138
  while (true) {
1050
1139
  const result = await adapter.fetchMessages(threadId, {
1051
1140
  limit: 100,
@@ -1053,6 +1142,7 @@ var ThreadImpl = class _ThreadImpl {
1053
1142
  direction: "forward"
1054
1143
  });
1055
1144
  for (const message of result.messages) {
1145
+ yieldedAny = true;
1056
1146
  yield message;
1057
1147
  }
1058
1148
  if (!result.nextCursor || result.messages.length === 0) {
@@ -1060,6 +1150,12 @@ var ThreadImpl = class _ThreadImpl {
1060
1150
  }
1061
1151
  cursor = result.nextCursor;
1062
1152
  }
1153
+ if (!yieldedAny && messageHistory) {
1154
+ const cached = await messageHistory.getMessages(threadId);
1155
+ for (const message of cached) {
1156
+ yield message;
1157
+ }
1158
+ }
1063
1159
  }
1064
1160
  };
1065
1161
  }
@@ -1096,6 +1192,9 @@ var ThreadImpl = class _ThreadImpl {
1096
1192
  postable,
1097
1193
  rawMessage.threadId
1098
1194
  );
1195
+ if (this._messageHistory) {
1196
+ await this._messageHistory.append(this.id, new Message(result));
1197
+ }
1099
1198
  return result;
1100
1199
  }
1101
1200
  async postEphemeral(user, message, options) {
@@ -1183,11 +1282,15 @@ var ThreadImpl = class _ThreadImpl {
1183
1282
  }
1184
1283
  };
1185
1284
  const raw = await this.adapter.stream(this.id, wrappedStream, options);
1186
- return this.createSentMessage(
1285
+ const sent = this.createSentMessage(
1187
1286
  raw.id,
1188
1287
  { markdown: accumulated },
1189
1288
  raw.threadId
1190
1289
  );
1290
+ if (this._messageHistory) {
1291
+ await this._messageHistory.append(this.id, new Message(sent));
1292
+ }
1293
+ return sent;
1191
1294
  }
1192
1295
  const textOnlyStream = {
1193
1296
  [Symbol.asyncIterator]: () => {
@@ -1299,15 +1402,28 @@ var ThreadImpl = class _ThreadImpl {
1299
1402
  markdown: accumulated
1300
1403
  });
1301
1404
  }
1302
- return this.createSentMessage(
1405
+ const sent = this.createSentMessage(
1303
1406
  msg.id,
1304
1407
  { markdown: accumulated },
1305
1408
  threadIdForEdits
1306
1409
  );
1410
+ if (this._messageHistory) {
1411
+ await this._messageHistory.append(this.id, new Message(sent));
1412
+ }
1413
+ return sent;
1307
1414
  }
1308
1415
  async refresh() {
1309
1416
  const result = await this.adapter.fetchMessages(this.id, { limit: 50 });
1310
- this._recentMessages = result.messages;
1417
+ if (result.messages.length > 0) {
1418
+ this._recentMessages = result.messages;
1419
+ } else if (this._messageHistory) {
1420
+ this._recentMessages = await this._messageHistory.getMessages(
1421
+ this.id,
1422
+ 50
1423
+ );
1424
+ } else {
1425
+ this._recentMessages = [];
1426
+ }
1311
1427
  }
1312
1428
  mentionUser(userId) {
1313
1429
  return `<@${userId}>`;
@@ -1564,7 +1680,9 @@ var Chat = class {
1564
1680
  _fallbackStreamingPlaceholderText;
1565
1681
  _dedupeTtlMs;
1566
1682
  _onLockConflict;
1683
+ _messageHistory;
1567
1684
  mentionHandlers = [];
1685
+ directMessageHandlers = [];
1568
1686
  messagePatterns = [];
1569
1687
  subscribedMessageHandlers = [];
1570
1688
  reactionHandlers = [];
@@ -1593,6 +1711,10 @@ var Chat = class {
1593
1711
  this._fallbackStreamingPlaceholderText = config.fallbackStreamingPlaceholderText !== void 0 ? config.fallbackStreamingPlaceholderText : "...";
1594
1712
  this._dedupeTtlMs = config.dedupeTtlMs ?? DEDUPE_TTL_MS;
1595
1713
  this._onLockConflict = config.onLockConflict;
1714
+ this._messageHistory = new MessageHistoryCache(
1715
+ this._stateAdapter,
1716
+ config.messageHistory
1717
+ );
1596
1718
  if (typeof config.logger === "string") {
1597
1719
  this.logger = new ConsoleLogger(config.logger);
1598
1720
  } else {
@@ -1699,6 +1821,27 @@ var Chat = class {
1699
1821
  this.mentionHandlers.push(handler);
1700
1822
  this.logger.debug("Registered mention handler");
1701
1823
  }
1824
+ /**
1825
+ * Register a handler for direct messages.
1826
+ *
1827
+ * Called when a message is received in a DM thread that is not subscribed.
1828
+ * If no `onDirectMessage` handlers are registered, DMs fall through to
1829
+ * `onNewMention` for backward compatibility.
1830
+ *
1831
+ * @param handler - Handler called for DM messages
1832
+ *
1833
+ * @example
1834
+ * ```typescript
1835
+ * chat.onDirectMessage(async (thread, message) => {
1836
+ * await thread.subscribe();
1837
+ * await thread.post("Thanks for the DM!");
1838
+ * });
1839
+ * ```
1840
+ */
1841
+ onDirectMessage(handler) {
1842
+ this.directMessageHandlers.push(handler);
1843
+ this.logger.debug("Registered direct message handler");
1844
+ }
1702
1845
  /**
1703
1846
  * Register a handler for messages matching a regex pattern.
1704
1847
  *
@@ -2525,6 +2668,14 @@ var Chat = class {
2525
2668
  });
2526
2669
  return;
2527
2670
  }
2671
+ if (adapter.persistMessageHistory) {
2672
+ const channelId = adapter.channelIdFromThreadId(threadId);
2673
+ const appends = [this._messageHistory.append(threadId, message)];
2674
+ if (channelId !== threadId) {
2675
+ appends.push(this._messageHistory.append(channelId, message));
2676
+ }
2677
+ await Promise.all(appends);
2678
+ }
2528
2679
  let lock = await this._stateAdapter.acquireLock(
2529
2680
  threadId,
2530
2681
  DEFAULT_LOCK_TTL_MS
@@ -2561,6 +2712,21 @@ var Chat = class {
2561
2712
  message,
2562
2713
  isSubscribed
2563
2714
  );
2715
+ const isDM = adapter.isDM?.(threadId) ?? false;
2716
+ if (isDM && this.directMessageHandlers.length > 0) {
2717
+ this.logger.debug("Direct message received - calling handlers", {
2718
+ threadId,
2719
+ handlerCount: this.directMessageHandlers.length
2720
+ });
2721
+ const channel = thread.channel;
2722
+ for (const handler of this.directMessageHandlers) {
2723
+ await handler(thread, message, channel);
2724
+ }
2725
+ return;
2726
+ }
2727
+ if (isDM) {
2728
+ message.isMention = true;
2729
+ }
2564
2730
  if (isSubscribed) {
2565
2731
  this.logger.debug("Message in subscribed thread - calling handlers", {
2566
2732
  threadId,
@@ -2623,7 +2789,8 @@ var Chat = class {
2623
2789
  isDM,
2624
2790
  currentMessage: initialMessage,
2625
2791
  streamingUpdateIntervalMs: this._streamingUpdateIntervalMs,
2626
- fallbackStreamingPlaceholderText: this._fallbackStreamingPlaceholderText
2792
+ fallbackStreamingPlaceholderText: this._fallbackStreamingPlaceholderText,
2793
+ messageHistory: adapter.persistMessageHistory ? this._messageHistory : void 0
2627
2794
  });
2628
2795
  }
2629
2796
  /**
@@ -2922,6 +3089,8 @@ function convertEmojiPlaceholders(text2, platform, resolver = defaultEmojiResolv
2922
3089
  return resolver.toGChat(emojiName);
2923
3090
  case "linear":
2924
3091
  return resolver.toGChat(emojiName);
3092
+ case "whatsapp":
3093
+ return resolver.toGChat(emojiName);
2925
3094
  default:
2926
3095
  return resolver.toGChat(emojiName);
2927
3096
  }
@@ -3101,6 +3270,7 @@ export {
3101
3270
  LinkButton2 as LinkButton,
3102
3271
  LockError,
3103
3272
  Message,
3273
+ MessageHistoryCache,
3104
3274
  Modal2 as Modal,
3105
3275
  NotImplementedError,
3106
3276
  RadioSelect2 as RadioSelect,
@@ -3157,6 +3327,7 @@ export {
3157
3327
  tableElementToAscii,
3158
3328
  tableToAscii,
3159
3329
  text,
3330
+ toAiMessages,
3160
3331
  toCardElement2 as toCardElement,
3161
3332
  toModalElement2 as toModalElement,
3162
3333
  toPlainText,