chat 4.4.0 → 4.5.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
@@ -22,6 +22,153 @@ import {
22
22
  toModalElement
23
23
  } from "./chunk-VJ2RPXCK.js";
24
24
 
25
+ // src/chat-singleton.ts
26
+ var _singleton = null;
27
+ function setChatSingleton(chat) {
28
+ _singleton = chat;
29
+ }
30
+ function getChatSingleton() {
31
+ if (!_singleton) {
32
+ throw new Error(
33
+ "No Chat singleton registered. Call chat.registerSingleton() first."
34
+ );
35
+ }
36
+ return _singleton;
37
+ }
38
+ function hasChatSingleton() {
39
+ return _singleton !== null;
40
+ }
41
+
42
+ // src/message.ts
43
+ import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";
44
+ var Message = class _Message {
45
+ /** Unique message ID */
46
+ id;
47
+ /** Thread this message belongs to */
48
+ threadId;
49
+ /** Plain text content (all formatting stripped) */
50
+ text;
51
+ /**
52
+ * Structured formatting as an AST (mdast Root).
53
+ * This is the canonical representation - use this for processing.
54
+ * Use `stringifyMarkdown(message.formatted)` to get markdown string.
55
+ */
56
+ formatted;
57
+ /** Platform-specific raw payload (escape hatch) */
58
+ raw;
59
+ /** Message author */
60
+ author;
61
+ /** Message metadata */
62
+ metadata;
63
+ /** Attachments */
64
+ attachments;
65
+ /**
66
+ * Whether the bot is @-mentioned in this message.
67
+ *
68
+ * This is set by the Chat SDK before passing the message to handlers.
69
+ * It checks for `@username` in the message text using the adapter's
70
+ * configured `userName` and optional `botUserId`.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * chat.onSubscribedMessage(async (thread, message) => {
75
+ * if (message.isMention) {
76
+ * await thread.post("You mentioned me!");
77
+ * }
78
+ * });
79
+ * ```
80
+ */
81
+ isMention;
82
+ constructor(data) {
83
+ this.id = data.id;
84
+ this.threadId = data.threadId;
85
+ this.text = data.text;
86
+ this.formatted = data.formatted;
87
+ this.raw = data.raw;
88
+ this.author = data.author;
89
+ this.metadata = data.metadata;
90
+ this.attachments = data.attachments;
91
+ this.isMention = data.isMention;
92
+ }
93
+ /**
94
+ * Serialize the message to a plain JSON object.
95
+ * Use this to pass message data to external systems like workflow engines.
96
+ *
97
+ * Note: Attachment `data` (Buffer) and `fetchData` (function) are omitted
98
+ * as they're not serializable.
99
+ */
100
+ toJSON() {
101
+ return {
102
+ _type: "chat:Message",
103
+ id: this.id,
104
+ threadId: this.threadId,
105
+ text: this.text,
106
+ formatted: this.formatted,
107
+ raw: this.raw,
108
+ author: {
109
+ userId: this.author.userId,
110
+ userName: this.author.userName,
111
+ fullName: this.author.fullName,
112
+ isBot: this.author.isBot,
113
+ isMe: this.author.isMe
114
+ },
115
+ metadata: {
116
+ dateSent: this.metadata.dateSent.toISOString(),
117
+ edited: this.metadata.edited,
118
+ editedAt: this.metadata.editedAt?.toISOString()
119
+ },
120
+ attachments: this.attachments.map((att) => ({
121
+ type: att.type,
122
+ url: att.url,
123
+ name: att.name,
124
+ mimeType: att.mimeType,
125
+ size: att.size,
126
+ width: att.width,
127
+ height: att.height
128
+ })),
129
+ isMention: this.isMention
130
+ };
131
+ }
132
+ /**
133
+ * Reconstruct a Message from serialized JSON data.
134
+ * Converts ISO date strings back to Date objects.
135
+ */
136
+ static fromJSON(json) {
137
+ return new _Message({
138
+ id: json.id,
139
+ threadId: json.threadId,
140
+ text: json.text,
141
+ formatted: json.formatted,
142
+ raw: json.raw,
143
+ author: json.author,
144
+ metadata: {
145
+ dateSent: new Date(json.metadata.dateSent),
146
+ edited: json.metadata.edited,
147
+ editedAt: json.metadata.editedAt ? new Date(json.metadata.editedAt) : void 0
148
+ },
149
+ attachments: json.attachments,
150
+ isMention: json.isMention
151
+ });
152
+ }
153
+ /**
154
+ * Serialize a Message instance for @workflow/serde.
155
+ * This static method is automatically called by workflow serialization.
156
+ */
157
+ static [WORKFLOW_SERIALIZE](instance) {
158
+ return instance.toJSON();
159
+ }
160
+ /**
161
+ * Deserialize a Message from @workflow/serde.
162
+ * This static method is automatically called by workflow deserialization.
163
+ */
164
+ static [WORKFLOW_DESERIALIZE](data) {
165
+ return _Message.fromJSON(data);
166
+ }
167
+ };
168
+
169
+ // src/thread.ts
170
+ import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE2, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE2 } from "@workflow/serde";
171
+
25
172
  // src/markdown.ts
26
173
  import { toString } from "mdast-util-to-string";
27
174
  import remarkGfm from "remark-gfm";
@@ -301,16 +448,23 @@ var ConsoleLogger = class _ConsoleLogger {
301
448
  var THREAD_STATE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
302
449
 
303
450
  // src/thread.ts
451
+ function isLazyConfig(config) {
452
+ return "adapterName" in config && !("adapter" in config);
453
+ }
304
454
  var THREAD_STATE_KEY_PREFIX = "thread-state:";
305
455
  function isAsyncIterable(value) {
306
456
  return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
307
457
  }
308
- var ThreadImpl = class {
458
+ var ThreadImpl = class _ThreadImpl {
309
459
  id;
310
- adapter;
311
460
  channelId;
312
461
  isDM;
313
- _stateAdapter;
462
+ /** Direct adapter instance (if provided) */
463
+ _adapter;
464
+ /** Adapter name for lazy resolution */
465
+ _adapterName;
466
+ /** Direct state adapter instance (if provided) */
467
+ _stateAdapterInstance;
314
468
  _recentMessages = [];
315
469
  _isSubscribedContext;
316
470
  /** Current message context for streaming - provides userId/teamId */
@@ -319,17 +473,54 @@ var ThreadImpl = class {
319
473
  _streamingUpdateIntervalMs;
320
474
  constructor(config) {
321
475
  this.id = config.id;
322
- this.adapter = config.adapter;
323
476
  this.channelId = config.channelId;
324
477
  this.isDM = config.isDM ?? false;
325
- this._stateAdapter = config.stateAdapter;
326
478
  this._isSubscribedContext = config.isSubscribedContext ?? false;
327
479
  this._currentMessage = config.currentMessage;
328
480
  this._streamingUpdateIntervalMs = config.streamingUpdateIntervalMs ?? 500;
481
+ if (isLazyConfig(config)) {
482
+ this._adapterName = config.adapterName;
483
+ } else {
484
+ this._adapter = config.adapter;
485
+ this._stateAdapterInstance = config.stateAdapter;
486
+ }
329
487
  if (config.initialMessage) {
330
488
  this._recentMessages = [config.initialMessage];
331
489
  }
332
490
  }
491
+ /**
492
+ * Get the adapter for this thread.
493
+ * If created with lazy config, resolves from Chat singleton on first access.
494
+ */
495
+ get adapter() {
496
+ if (this._adapter) {
497
+ return this._adapter;
498
+ }
499
+ if (!this._adapterName) {
500
+ throw new Error("Thread has no adapter configured");
501
+ }
502
+ const chat = getChatSingleton();
503
+ const adapter = chat.getAdapter(this._adapterName);
504
+ if (!adapter) {
505
+ throw new Error(
506
+ `Adapter "${this._adapterName}" not found in Chat singleton`
507
+ );
508
+ }
509
+ this._adapter = adapter;
510
+ return adapter;
511
+ }
512
+ /**
513
+ * Get the state adapter for this thread.
514
+ * If created with lazy config, resolves from Chat singleton on first access.
515
+ */
516
+ get _stateAdapter() {
517
+ if (this._stateAdapterInstance) {
518
+ return this._stateAdapterInstance;
519
+ }
520
+ const chat = getChatSingleton();
521
+ this._stateAdapterInstance = chat.getState();
522
+ return this._stateAdapterInstance;
523
+ }
333
524
  get recentMessages() {
334
525
  return this._recentMessages;
335
526
  }
@@ -412,6 +603,37 @@ var ThreadImpl = class {
412
603
  const rawMessage = await this.adapter.postMessage(this.id, postable);
413
604
  return this.createSentMessage(rawMessage.id, postable, rawMessage.threadId);
414
605
  }
606
+ async postEphemeral(user, message, options) {
607
+ const { fallbackToDM } = options;
608
+ const userId = typeof user === "string" ? user : user.userId;
609
+ let postable;
610
+ if (isJSX(message)) {
611
+ const card = toCardElement(message);
612
+ if (!card) {
613
+ throw new Error("Invalid JSX element: must be a Card element");
614
+ }
615
+ postable = card;
616
+ } else {
617
+ postable = message;
618
+ }
619
+ if (this.adapter.postEphemeral) {
620
+ return this.adapter.postEphemeral(this.id, userId, postable);
621
+ }
622
+ if (!fallbackToDM) {
623
+ return null;
624
+ }
625
+ if (this.adapter.openDM) {
626
+ const dmThreadId = await this.adapter.openDM(userId);
627
+ const result = await this.adapter.postMessage(dmThreadId, postable);
628
+ return {
629
+ id: result.id,
630
+ threadId: dmThreadId,
631
+ usedFallback: true,
632
+ raw: result.raw
633
+ };
634
+ }
635
+ return null;
636
+ }
415
637
  /**
416
638
  * Handle streaming from an AsyncIterable.
417
639
  * Uses adapter's native streaming if available, otherwise falls back to post+edit.
@@ -507,6 +729,65 @@ var ThreadImpl = class {
507
729
  mentionUser(userId) {
508
730
  return `<@${userId}>`;
509
731
  }
732
+ /**
733
+ * Serialize the thread to a plain JSON object.
734
+ * Use this to pass thread data to external systems like workflow engines.
735
+ *
736
+ * @example
737
+ * ```typescript
738
+ * // Pass to a workflow
739
+ * await workflow.start("my-workflow", {
740
+ * thread: thread.toJSON(),
741
+ * message: serializeMessage(message),
742
+ * });
743
+ * ```
744
+ */
745
+ toJSON() {
746
+ return {
747
+ _type: "chat:Thread",
748
+ id: this.id,
749
+ channelId: this.channelId,
750
+ isDM: this.isDM,
751
+ adapterName: this.adapter.name
752
+ };
753
+ }
754
+ /**
755
+ * Reconstruct a Thread from serialized JSON data.
756
+ *
757
+ * Reconstructs a ThreadImpl from serialized data.
758
+ * Uses lazy resolution from Chat.getSingleton() for adapter and state.
759
+ *
760
+ * @param json - Serialized thread data
761
+ * @requires Call `chat.registerSingleton()` before deserializing threads
762
+ *
763
+ * @example
764
+ * ```typescript
765
+ * const thread = ThreadImpl.fromJSON(serializedThread);
766
+ * ```
767
+ */
768
+ static fromJSON(json) {
769
+ return new _ThreadImpl({
770
+ id: json.id,
771
+ adapterName: json.adapterName,
772
+ channelId: json.channelId,
773
+ isDM: json.isDM
774
+ });
775
+ }
776
+ /**
777
+ * Serialize a ThreadImpl instance for @workflow/serde.
778
+ * This static method is automatically called by workflow serialization.
779
+ */
780
+ static [WORKFLOW_SERIALIZE2](instance) {
781
+ return instance.toJSON();
782
+ }
783
+ /**
784
+ * Deserialize a ThreadImpl from @workflow/serde.
785
+ * Uses lazy adapter resolution from Chat.getSingleton().
786
+ * Requires chat.registerSingleton() to have been called.
787
+ */
788
+ static [WORKFLOW_DESERIALIZE2](data) {
789
+ return _ThreadImpl.fromJSON(data);
790
+ }
510
791
  createSentMessage(messageId, postable, threadIdOverride) {
511
792
  const adapter = this.adapter;
512
793
  const threadId = threadIdOverride || this.id;
@@ -531,6 +812,9 @@ var ThreadImpl = class {
531
812
  edited: false
532
813
  },
533
814
  attachments,
815
+ toJSON() {
816
+ return new Message(this).toJSON();
817
+ },
534
818
  async edit(newContent) {
535
819
  let postable2 = newContent;
536
820
  if (isJSX(newContent)) {
@@ -609,6 +893,36 @@ function extractMessageContent(message) {
609
893
  var DEFAULT_LOCK_TTL_MS = 3e4;
610
894
  var DEDUPE_TTL_MS = 6e4;
611
895
  var Chat = class {
896
+ /**
897
+ * Register this Chat instance as the global singleton.
898
+ * Required for Thread deserialization via @workflow/serde.
899
+ *
900
+ * @example
901
+ * ```typescript
902
+ * const chat = new Chat({ ... });
903
+ * chat.registerSingleton();
904
+ *
905
+ * // Now threads can be deserialized without passing chat explicitly
906
+ * const thread = ThreadImpl.fromJSON(serializedThread);
907
+ * ```
908
+ */
909
+ registerSingleton() {
910
+ setChatSingleton(this);
911
+ return this;
912
+ }
913
+ /**
914
+ * Get the registered singleton Chat instance.
915
+ * Throws if no singleton has been registered.
916
+ */
917
+ static getSingleton() {
918
+ return getChatSingleton();
919
+ }
920
+ /**
921
+ * Check if a singleton has been registered.
922
+ */
923
+ static hasSingleton() {
924
+ return hasChatSingleton();
925
+ }
612
926
  adapters;
613
927
  _stateAdapter;
614
928
  userName;
@@ -841,6 +1155,40 @@ var Chat = class {
841
1155
  getAdapter(name) {
842
1156
  return this.adapters.get(name);
843
1157
  }
1158
+ /**
1159
+ * Get a JSON.parse reviver function that automatically deserializes
1160
+ * chat:Thread and chat:Message objects.
1161
+ *
1162
+ * Use this when parsing JSON that contains serialized Thread or Message objects
1163
+ * (e.g., from workflow engine payloads).
1164
+ *
1165
+ * @returns A reviver function for JSON.parse
1166
+ *
1167
+ * @example
1168
+ * ```typescript
1169
+ * // Parse workflow payload with automatic deserialization
1170
+ * const data = JSON.parse(payload, chat.reviver());
1171
+ *
1172
+ * // data.thread is now a ThreadImpl instance
1173
+ * // data.message is now a Message object with Date fields restored
1174
+ * await data.thread.post("Hello from workflow!");
1175
+ * ```
1176
+ */
1177
+ reviver() {
1178
+ this.registerSingleton();
1179
+ return function reviver(_key, value) {
1180
+ if (value && typeof value === "object" && "_type" in value) {
1181
+ const typed = value;
1182
+ if (typed._type === "chat:Thread") {
1183
+ return ThreadImpl.fromJSON(value);
1184
+ }
1185
+ if (typed._type === "chat:Message") {
1186
+ return Message.fromJSON(value);
1187
+ }
1188
+ }
1189
+ return value;
1190
+ };
1191
+ }
844
1192
  // ChatInstance interface implementations
845
1193
  /**
846
1194
  * Process an incoming message from an adapter.
@@ -1728,6 +2076,7 @@ export {
1728
2076
  Fields2 as Fields,
1729
2077
  Image2 as Image,
1730
2078
  LockError,
2079
+ Message,
1731
2080
  Modal2 as Modal,
1732
2081
  NotImplementedError,
1733
2082
  RateLimitError,