chat 4.11.0 → 4.13.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
@@ -24,6 +24,9 @@ import {
24
24
  toModalElement
25
25
  } from "./chunk-WKJEG4FE.js";
26
26
 
27
+ // src/channel.ts
28
+ import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE2, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE2 } from "@workflow/serde";
29
+
27
30
  // src/chat-singleton.ts
28
31
  var _singleton = null;
29
32
  function setChatSingleton(chat) {
@@ -41,136 +44,6 @@ function hasChatSingleton() {
41
44
  return _singleton !== null;
42
45
  }
43
46
 
44
- // src/message.ts
45
- import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";
46
- var Message = class _Message {
47
- /** Unique message ID */
48
- id;
49
- /** Thread this message belongs to */
50
- threadId;
51
- /** Plain text content (all formatting stripped) */
52
- text;
53
- /**
54
- * Structured formatting as an AST (mdast Root).
55
- * This is the canonical representation - use this for processing.
56
- * Use `stringifyMarkdown(message.formatted)` to get markdown string.
57
- */
58
- formatted;
59
- /** Platform-specific raw payload (escape hatch) */
60
- raw;
61
- /** Message author */
62
- author;
63
- /** Message metadata */
64
- metadata;
65
- /** Attachments */
66
- attachments;
67
- /**
68
- * Whether the bot is @-mentioned in this message.
69
- *
70
- * This is set by the Chat SDK before passing the message to handlers.
71
- * It checks for `@username` in the message text using the adapter's
72
- * configured `userName` and optional `botUserId`.
73
- *
74
- * @example
75
- * ```typescript
76
- * chat.onSubscribedMessage(async (thread, message) => {
77
- * if (message.isMention) {
78
- * await thread.post("You mentioned me!");
79
- * }
80
- * });
81
- * ```
82
- */
83
- isMention;
84
- constructor(data) {
85
- this.id = data.id;
86
- this.threadId = data.threadId;
87
- this.text = data.text;
88
- this.formatted = data.formatted;
89
- this.raw = data.raw;
90
- this.author = data.author;
91
- this.metadata = data.metadata;
92
- this.attachments = data.attachments;
93
- this.isMention = data.isMention;
94
- }
95
- /**
96
- * Serialize the message to a plain JSON object.
97
- * Use this to pass message data to external systems like workflow engines.
98
- *
99
- * Note: Attachment `data` (Buffer) and `fetchData` (function) are omitted
100
- * as they're not serializable.
101
- */
102
- toJSON() {
103
- return {
104
- _type: "chat:Message",
105
- id: this.id,
106
- threadId: this.threadId,
107
- text: this.text,
108
- formatted: this.formatted,
109
- raw: this.raw,
110
- author: {
111
- userId: this.author.userId,
112
- userName: this.author.userName,
113
- fullName: this.author.fullName,
114
- isBot: this.author.isBot,
115
- isMe: this.author.isMe
116
- },
117
- metadata: {
118
- dateSent: this.metadata.dateSent.toISOString(),
119
- edited: this.metadata.edited,
120
- editedAt: this.metadata.editedAt?.toISOString()
121
- },
122
- attachments: this.attachments.map((att) => ({
123
- type: att.type,
124
- url: att.url,
125
- name: att.name,
126
- mimeType: att.mimeType,
127
- size: att.size,
128
- width: att.width,
129
- height: att.height
130
- })),
131
- isMention: this.isMention
132
- };
133
- }
134
- /**
135
- * Reconstruct a Message from serialized JSON data.
136
- * Converts ISO date strings back to Date objects.
137
- */
138
- static fromJSON(json) {
139
- return new _Message({
140
- id: json.id,
141
- threadId: json.threadId,
142
- text: json.text,
143
- formatted: json.formatted,
144
- raw: json.raw,
145
- author: json.author,
146
- metadata: {
147
- dateSent: new Date(json.metadata.dateSent),
148
- edited: json.metadata.edited,
149
- editedAt: json.metadata.editedAt ? new Date(json.metadata.editedAt) : void 0
150
- },
151
- attachments: json.attachments,
152
- isMention: json.isMention
153
- });
154
- }
155
- /**
156
- * Serialize a Message instance for @workflow/serde.
157
- * This static method is automatically called by workflow serialization.
158
- */
159
- static [WORKFLOW_SERIALIZE](instance) {
160
- return instance.toJSON();
161
- }
162
- /**
163
- * Deserialize a Message from @workflow/serde.
164
- * This static method is automatically called by workflow deserialization.
165
- */
166
- static [WORKFLOW_DESERIALIZE](data) {
167
- return _Message.fromJSON(data);
168
- }
169
- };
170
-
171
- // src/thread.ts
172
- import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE2, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE2 } from "@workflow/serde";
173
-
174
47
  // src/markdown.ts
175
48
  import { toString } from "mdast-util-to-string";
176
49
  import remarkGfm from "remark-gfm";
@@ -380,6 +253,133 @@ var BaseFormatConverter = class {
380
253
  }
381
254
  };
382
255
 
256
+ // src/message.ts
257
+ import { WORKFLOW_DESERIALIZE, WORKFLOW_SERIALIZE } from "@workflow/serde";
258
+ var Message = class _Message {
259
+ /** Unique message ID */
260
+ id;
261
+ /** Thread this message belongs to */
262
+ threadId;
263
+ /** Plain text content (all formatting stripped) */
264
+ text;
265
+ /**
266
+ * Structured formatting as an AST (mdast Root).
267
+ * This is the canonical representation - use this for processing.
268
+ * Use `stringifyMarkdown(message.formatted)` to get markdown string.
269
+ */
270
+ formatted;
271
+ /** Platform-specific raw payload (escape hatch) */
272
+ raw;
273
+ /** Message author */
274
+ author;
275
+ /** Message metadata */
276
+ metadata;
277
+ /** Attachments */
278
+ attachments;
279
+ /**
280
+ * Whether the bot is @-mentioned in this message.
281
+ *
282
+ * This is set by the Chat SDK before passing the message to handlers.
283
+ * It checks for `@username` in the message text using the adapter's
284
+ * configured `userName` and optional `botUserId`.
285
+ *
286
+ * @example
287
+ * ```typescript
288
+ * chat.onSubscribedMessage(async (thread, message) => {
289
+ * if (message.isMention) {
290
+ * await thread.post("You mentioned me!");
291
+ * }
292
+ * });
293
+ * ```
294
+ */
295
+ isMention;
296
+ constructor(data) {
297
+ this.id = data.id;
298
+ this.threadId = data.threadId;
299
+ this.text = data.text;
300
+ this.formatted = data.formatted;
301
+ this.raw = data.raw;
302
+ this.author = data.author;
303
+ this.metadata = data.metadata;
304
+ this.attachments = data.attachments;
305
+ this.isMention = data.isMention;
306
+ }
307
+ /**
308
+ * Serialize the message to a plain JSON object.
309
+ * Use this to pass message data to external systems like workflow engines.
310
+ *
311
+ * Note: Attachment `data` (Buffer) and `fetchData` (function) are omitted
312
+ * as they're not serializable.
313
+ */
314
+ toJSON() {
315
+ return {
316
+ _type: "chat:Message",
317
+ id: this.id,
318
+ threadId: this.threadId,
319
+ text: this.text,
320
+ formatted: this.formatted,
321
+ raw: this.raw,
322
+ author: {
323
+ userId: this.author.userId,
324
+ userName: this.author.userName,
325
+ fullName: this.author.fullName,
326
+ isBot: this.author.isBot,
327
+ isMe: this.author.isMe
328
+ },
329
+ metadata: {
330
+ dateSent: this.metadata.dateSent.toISOString(),
331
+ edited: this.metadata.edited,
332
+ editedAt: this.metadata.editedAt?.toISOString()
333
+ },
334
+ attachments: this.attachments.map((att) => ({
335
+ type: att.type,
336
+ url: att.url,
337
+ name: att.name,
338
+ mimeType: att.mimeType,
339
+ size: att.size,
340
+ width: att.width,
341
+ height: att.height
342
+ })),
343
+ isMention: this.isMention
344
+ };
345
+ }
346
+ /**
347
+ * Reconstruct a Message from serialized JSON data.
348
+ * Converts ISO date strings back to Date objects.
349
+ */
350
+ static fromJSON(json) {
351
+ return new _Message({
352
+ id: json.id,
353
+ threadId: json.threadId,
354
+ text: json.text,
355
+ formatted: json.formatted,
356
+ raw: json.raw,
357
+ author: json.author,
358
+ metadata: {
359
+ dateSent: new Date(json.metadata.dateSent),
360
+ edited: json.metadata.edited,
361
+ editedAt: json.metadata.editedAt ? new Date(json.metadata.editedAt) : void 0
362
+ },
363
+ attachments: json.attachments,
364
+ isMention: json.isMention
365
+ });
366
+ }
367
+ /**
368
+ * Serialize a Message instance for @workflow/serde.
369
+ * This static method is automatically called by workflow serialization.
370
+ */
371
+ static [WORKFLOW_SERIALIZE](instance) {
372
+ return instance.toJSON();
373
+ }
374
+ /**
375
+ * Deserialize a Message from @workflow/serde.
376
+ * This static method is automatically called by workflow deserialization.
377
+ */
378
+ static [WORKFLOW_DESERIALIZE](data) {
379
+ return _Message.fromJSON(data);
380
+ }
381
+ };
382
+
383
383
  // src/errors.ts
384
384
  var ChatError = class extends Error {
385
385
  constructor(message, code, cause) {
@@ -409,52 +409,377 @@ var NotImplementedError = class extends ChatError {
409
409
  this.name = "NotImplementedError";
410
410
  }
411
411
  };
412
-
413
- // src/logger.ts
414
- var ConsoleLogger = class _ConsoleLogger {
415
- constructor(level = "info", prefix = "chat-sdk") {
416
- this.level = level;
417
- this.prefix = prefix;
412
+
413
+ // src/logger.ts
414
+ var ConsoleLogger = class _ConsoleLogger {
415
+ constructor(level = "info", prefix = "chat-sdk") {
416
+ this.level = level;
417
+ this.prefix = prefix;
418
+ }
419
+ prefix;
420
+ shouldLog(level) {
421
+ const levels = ["debug", "info", "warn", "error", "silent"];
422
+ return levels.indexOf(level) >= levels.indexOf(this.level);
423
+ }
424
+ child(prefix) {
425
+ return new _ConsoleLogger(this.level, `${this.prefix}:${prefix}`);
426
+ }
427
+ // eslint-disable-next-line no-console
428
+ debug(message, ...args) {
429
+ if (this.shouldLog("debug"))
430
+ console.debug(`[${this.prefix}] ${message}`, ...args);
431
+ }
432
+ // eslint-disable-next-line no-console
433
+ info(message, ...args) {
434
+ if (this.shouldLog("info"))
435
+ console.info(`[${this.prefix}] ${message}`, ...args);
436
+ }
437
+ // eslint-disable-next-line no-console
438
+ warn(message, ...args) {
439
+ if (this.shouldLog("warn"))
440
+ console.warn(`[${this.prefix}] ${message}`, ...args);
441
+ }
442
+ // eslint-disable-next-line no-console
443
+ error(message, ...args) {
444
+ if (this.shouldLog("error"))
445
+ console.error(`[${this.prefix}] ${message}`, ...args);
446
+ }
447
+ };
448
+
449
+ // src/types.ts
450
+ var THREAD_STATE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
451
+
452
+ // src/channel.ts
453
+ var CHANNEL_STATE_KEY_PREFIX = "channel-state:";
454
+ function isLazyConfig(config) {
455
+ return "adapterName" in config && !("adapter" in config);
456
+ }
457
+ function isAsyncIterable(value) {
458
+ return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
459
+ }
460
+ var ChannelImpl = class _ChannelImpl {
461
+ id;
462
+ isDM;
463
+ _adapter;
464
+ _adapterName;
465
+ _stateAdapterInstance;
466
+ _name = null;
467
+ constructor(config) {
468
+ this.id = config.id;
469
+ this.isDM = config.isDM ?? false;
470
+ if (isLazyConfig(config)) {
471
+ this._adapterName = config.adapterName;
472
+ } else {
473
+ this._adapter = config.adapter;
474
+ this._stateAdapterInstance = config.stateAdapter;
475
+ }
476
+ }
477
+ get adapter() {
478
+ if (this._adapter) {
479
+ return this._adapter;
480
+ }
481
+ if (!this._adapterName) {
482
+ throw new Error("Channel has no adapter configured");
483
+ }
484
+ const chat = getChatSingleton();
485
+ const adapter = chat.getAdapter(this._adapterName);
486
+ if (!adapter) {
487
+ throw new Error(
488
+ `Adapter "${this._adapterName}" not found in Chat singleton`
489
+ );
490
+ }
491
+ this._adapter = adapter;
492
+ return adapter;
493
+ }
494
+ get _stateAdapter() {
495
+ if (this._stateAdapterInstance) {
496
+ return this._stateAdapterInstance;
497
+ }
498
+ const chat = getChatSingleton();
499
+ this._stateAdapterInstance = chat.getState();
500
+ return this._stateAdapterInstance;
501
+ }
502
+ get name() {
503
+ return this._name;
504
+ }
505
+ get state() {
506
+ return this._stateAdapter.get(
507
+ `${CHANNEL_STATE_KEY_PREFIX}${this.id}`
508
+ );
509
+ }
510
+ async setState(newState, options) {
511
+ const key = `${CHANNEL_STATE_KEY_PREFIX}${this.id}`;
512
+ if (options?.replace) {
513
+ await this._stateAdapter.set(key, newState, THREAD_STATE_TTL_MS);
514
+ } else {
515
+ const existing = await this._stateAdapter.get(key);
516
+ const merged = { ...existing, ...newState };
517
+ await this._stateAdapter.set(key, merged, THREAD_STATE_TTL_MS);
518
+ }
519
+ }
520
+ /**
521
+ * Iterate messages newest first (backward from most recent).
522
+ * Uses adapter.fetchChannelMessages if available, otherwise falls back
523
+ * to adapter.fetchMessages with the channel ID.
524
+ */
525
+ get messages() {
526
+ const adapter = this.adapter;
527
+ const channelId = this.id;
528
+ return {
529
+ async *[Symbol.asyncIterator]() {
530
+ let cursor;
531
+ while (true) {
532
+ const fetchOptions = { cursor, direction: "backward" };
533
+ const result = adapter.fetchChannelMessages ? await adapter.fetchChannelMessages(channelId, fetchOptions) : await adapter.fetchMessages(channelId, fetchOptions);
534
+ const reversed = [...result.messages].reverse();
535
+ for (const message of reversed) {
536
+ yield message;
537
+ }
538
+ if (!result.nextCursor || result.messages.length === 0) {
539
+ break;
540
+ }
541
+ cursor = result.nextCursor;
542
+ }
543
+ }
544
+ };
545
+ }
546
+ /**
547
+ * Iterate threads in this channel, most recently active first.
548
+ */
549
+ threads() {
550
+ const adapter = this.adapter;
551
+ const channelId = this.id;
552
+ return {
553
+ async *[Symbol.asyncIterator]() {
554
+ if (!adapter.listThreads) {
555
+ return;
556
+ }
557
+ let cursor;
558
+ while (true) {
559
+ const result = await adapter.listThreads(channelId, {
560
+ cursor
561
+ });
562
+ for (const thread of result.threads) {
563
+ yield thread;
564
+ }
565
+ if (!result.nextCursor || result.threads.length === 0) {
566
+ break;
567
+ }
568
+ cursor = result.nextCursor;
569
+ }
570
+ }
571
+ };
572
+ }
573
+ async fetchMetadata() {
574
+ if (this.adapter.fetchChannelInfo) {
575
+ const info = await this.adapter.fetchChannelInfo(this.id);
576
+ this._name = info.name ?? null;
577
+ return info;
578
+ }
579
+ return {
580
+ id: this.id,
581
+ isDM: this.isDM,
582
+ metadata: {}
583
+ };
584
+ }
585
+ async post(message) {
586
+ if (isAsyncIterable(message)) {
587
+ let accumulated = "";
588
+ for await (const chunk of message) {
589
+ accumulated += chunk;
590
+ }
591
+ return this.postSingleMessage(accumulated);
592
+ }
593
+ let postable = message;
594
+ if (isJSX(message)) {
595
+ const card = toCardElement(message);
596
+ if (!card) {
597
+ throw new Error("Invalid JSX element: must be a Card element");
598
+ }
599
+ postable = card;
600
+ }
601
+ return this.postSingleMessage(postable);
602
+ }
603
+ async postSingleMessage(postable) {
604
+ const rawMessage = this.adapter.postChannelMessage ? await this.adapter.postChannelMessage(this.id, postable) : await this.adapter.postMessage(this.id, postable);
605
+ return this.createSentMessage(rawMessage.id, postable, rawMessage.threadId);
606
+ }
607
+ async postEphemeral(user, message, options) {
608
+ const { fallbackToDM } = options;
609
+ const userId = typeof user === "string" ? user : user.userId;
610
+ let postable;
611
+ if (isJSX(message)) {
612
+ const card = toCardElement(message);
613
+ if (!card) {
614
+ throw new Error("Invalid JSX element: must be a Card element");
615
+ }
616
+ postable = card;
617
+ } else {
618
+ postable = message;
619
+ }
620
+ if (this.adapter.postEphemeral) {
621
+ return this.adapter.postEphemeral(this.id, userId, postable);
622
+ }
623
+ if (!fallbackToDM) {
624
+ return null;
625
+ }
626
+ if (this.adapter.openDM) {
627
+ const dmThreadId = await this.adapter.openDM(userId);
628
+ const result = await this.adapter.postMessage(dmThreadId, postable);
629
+ return {
630
+ id: result.id,
631
+ threadId: dmThreadId,
632
+ usedFallback: true,
633
+ raw: result.raw
634
+ };
635
+ }
636
+ return null;
637
+ }
638
+ async startTyping() {
639
+ await this.adapter.startTyping(this.id);
640
+ }
641
+ mentionUser(userId) {
642
+ return `<@${userId}>`;
643
+ }
644
+ toJSON() {
645
+ return {
646
+ _type: "chat:Channel",
647
+ id: this.id,
648
+ adapterName: this.adapter.name,
649
+ isDM: this.isDM
650
+ };
651
+ }
652
+ static fromJSON(json, adapter) {
653
+ const channel = new _ChannelImpl({
654
+ id: json.id,
655
+ adapterName: json.adapterName,
656
+ isDM: json.isDM
657
+ });
658
+ if (adapter) {
659
+ channel._adapter = adapter;
660
+ }
661
+ return channel;
662
+ }
663
+ static [WORKFLOW_SERIALIZE2](instance) {
664
+ return instance.toJSON();
665
+ }
666
+ static [WORKFLOW_DESERIALIZE2](data) {
667
+ return _ChannelImpl.fromJSON(data);
668
+ }
669
+ createSentMessage(messageId, postable, threadIdOverride) {
670
+ const adapter = this.adapter;
671
+ const threadId = threadIdOverride || this.id;
672
+ const self = this;
673
+ const { plainText, formatted, attachments } = extractMessageContent(postable);
674
+ const sentMessage = {
675
+ id: messageId,
676
+ threadId,
677
+ text: plainText,
678
+ formatted,
679
+ raw: null,
680
+ author: {
681
+ userId: "self",
682
+ userName: adapter.userName,
683
+ fullName: adapter.userName,
684
+ isBot: true,
685
+ isMe: true
686
+ },
687
+ metadata: {
688
+ dateSent: /* @__PURE__ */ new Date(),
689
+ edited: false
690
+ },
691
+ attachments,
692
+ toJSON() {
693
+ return new Message(this).toJSON();
694
+ },
695
+ async edit(newContent) {
696
+ let editPostable = newContent;
697
+ if (isJSX(newContent)) {
698
+ const card = toCardElement(newContent);
699
+ if (!card) {
700
+ throw new Error("Invalid JSX element: must be a Card element");
701
+ }
702
+ editPostable = card;
703
+ }
704
+ await adapter.editMessage(threadId, messageId, editPostable);
705
+ return self.createSentMessage(messageId, editPostable);
706
+ },
707
+ async delete() {
708
+ await adapter.deleteMessage(threadId, messageId);
709
+ },
710
+ async addReaction(emoji2) {
711
+ await adapter.addReaction(threadId, messageId, emoji2);
712
+ },
713
+ async removeReaction(emoji2) {
714
+ await adapter.removeReaction(threadId, messageId, emoji2);
715
+ }
716
+ };
717
+ return sentMessage;
718
+ }
719
+ };
720
+ function deriveChannelId(adapter, threadId) {
721
+ if (adapter.channelIdFromThreadId) {
722
+ return adapter.channelIdFromThreadId(threadId);
418
723
  }
419
- prefix;
420
- shouldLog(level) {
421
- const levels = ["debug", "info", "warn", "error", "silent"];
422
- return levels.indexOf(level) >= levels.indexOf(this.level);
724
+ const parts = threadId.split(":");
725
+ return parts.slice(0, 2).join(":");
726
+ }
727
+ function extractMessageContent(message) {
728
+ if (typeof message === "string") {
729
+ return {
730
+ plainText: message,
731
+ formatted: root([paragraph([text(message)])]),
732
+ attachments: []
733
+ };
423
734
  }
424
- child(prefix) {
425
- return new _ConsoleLogger(this.level, `${this.prefix}:${prefix}`);
735
+ if ("raw" in message) {
736
+ return {
737
+ plainText: message.raw,
738
+ formatted: root([paragraph([text(message.raw)])]),
739
+ attachments: message.attachments || []
740
+ };
426
741
  }
427
- // eslint-disable-next-line no-console
428
- debug(message, ...args) {
429
- if (this.shouldLog("debug"))
430
- console.debug(`[${this.prefix}] ${message}`, ...args);
742
+ if ("markdown" in message) {
743
+ const ast = parseMarkdown(message.markdown);
744
+ return {
745
+ plainText: toPlainText(ast),
746
+ formatted: ast,
747
+ attachments: message.attachments || []
748
+ };
431
749
  }
432
- // eslint-disable-next-line no-console
433
- info(message, ...args) {
434
- if (this.shouldLog("info"))
435
- console.info(`[${this.prefix}] ${message}`, ...args);
750
+ if ("ast" in message) {
751
+ return {
752
+ plainText: toPlainText(message.ast),
753
+ formatted: message.ast,
754
+ attachments: message.attachments || []
755
+ };
436
756
  }
437
- // eslint-disable-next-line no-console
438
- warn(message, ...args) {
439
- if (this.shouldLog("warn"))
440
- console.warn(`[${this.prefix}] ${message}`, ...args);
757
+ if ("card" in message) {
758
+ const fallbackText = message.fallbackText || cardToFallbackText(message.card);
759
+ return {
760
+ plainText: fallbackText,
761
+ formatted: root([paragraph([text(fallbackText)])]),
762
+ attachments: []
763
+ };
441
764
  }
442
- // eslint-disable-next-line no-console
443
- error(message, ...args) {
444
- if (this.shouldLog("error"))
445
- console.error(`[${this.prefix}] ${message}`, ...args);
765
+ if ("type" in message && message.type === "card") {
766
+ const fallbackText = cardToFallbackText(message);
767
+ return {
768
+ plainText: fallbackText,
769
+ formatted: root([paragraph([text(fallbackText)])]),
770
+ attachments: []
771
+ };
446
772
  }
447
- };
448
-
449
- // src/types.ts
450
- var THREAD_STATE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
773
+ throw new Error("Invalid PostableMessage format");
774
+ }
451
775
 
452
776
  // src/thread.ts
453
- function isLazyConfig(config) {
777
+ import { WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE3, WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE3 } from "@workflow/serde";
778
+ function isLazyConfig2(config) {
454
779
  return "adapterName" in config && !("adapter" in config);
455
780
  }
456
781
  var THREAD_STATE_KEY_PREFIX = "thread-state:";
457
- function isAsyncIterable(value) {
782
+ function isAsyncIterable2(value) {
458
783
  return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
459
784
  }
460
785
  var ThreadImpl = class _ThreadImpl {
@@ -473,6 +798,8 @@ var ThreadImpl = class _ThreadImpl {
473
798
  _currentMessage;
474
799
  /** Update interval for fallback streaming */
475
800
  _streamingUpdateIntervalMs;
801
+ /** Cached channel instance */
802
+ _channel;
476
803
  constructor(config) {
477
804
  this.id = config.id;
478
805
  this.channelId = config.channelId;
@@ -480,7 +807,7 @@ var ThreadImpl = class _ThreadImpl {
480
807
  this._isSubscribedContext = config.isSubscribedContext ?? false;
481
808
  this._currentMessage = config.currentMessage;
482
809
  this._streamingUpdateIntervalMs = config.streamingUpdateIntervalMs ?? 500;
483
- if (isLazyConfig(config)) {
810
+ if (isLazyConfig2(config)) {
484
811
  this._adapterName = config.adapterName;
485
812
  } else {
486
813
  this._adapter = config.adapter;
@@ -552,6 +879,49 @@ var ThreadImpl = class _ThreadImpl {
552
879
  await this._stateAdapter.set(key, merged, THREAD_STATE_TTL_MS);
553
880
  }
554
881
  }
882
+ /**
883
+ * Get the Channel containing this thread.
884
+ * Lazy-created and cached.
885
+ */
886
+ get channel() {
887
+ if (!this._channel) {
888
+ const channelId = deriveChannelId(this.adapter, this.id);
889
+ this._channel = new ChannelImpl({
890
+ id: channelId,
891
+ adapter: this.adapter,
892
+ stateAdapter: this._stateAdapter,
893
+ isDM: this.isDM
894
+ });
895
+ }
896
+ return this._channel;
897
+ }
898
+ /**
899
+ * Iterate messages newest first (backward from most recent).
900
+ * Auto-paginates lazily.
901
+ */
902
+ get messages() {
903
+ const adapter = this.adapter;
904
+ const threadId = this.id;
905
+ return {
906
+ async *[Symbol.asyncIterator]() {
907
+ let cursor;
908
+ while (true) {
909
+ const result = await adapter.fetchMessages(threadId, {
910
+ cursor,
911
+ direction: "backward"
912
+ });
913
+ const reversed = [...result.messages].reverse();
914
+ for (const message of reversed) {
915
+ yield message;
916
+ }
917
+ if (!result.nextCursor || result.messages.length === 0) {
918
+ break;
919
+ }
920
+ cursor = result.nextCursor;
921
+ }
922
+ }
923
+ };
924
+ }
555
925
  get allMessages() {
556
926
  const adapter = this.adapter;
557
927
  const threadId = this.id;
@@ -591,7 +961,7 @@ var ThreadImpl = class _ThreadImpl {
591
961
  await this._stateAdapter.unsubscribe(this.id);
592
962
  }
593
963
  async post(message) {
594
- if (isAsyncIterable(message)) {
964
+ if (isAsyncIterable2(message)) {
595
965
  return this.handleStream(message);
596
966
  }
597
967
  let postable = message;
@@ -788,7 +1158,7 @@ var ThreadImpl = class _ThreadImpl {
788
1158
  * Serialize a ThreadImpl instance for @workflow/serde.
789
1159
  * This static method is automatically called by workflow serialization.
790
1160
  */
791
- static [WORKFLOW_SERIALIZE2](instance) {
1161
+ static [WORKFLOW_SERIALIZE3](instance) {
792
1162
  return instance.toJSON();
793
1163
  }
794
1164
  /**
@@ -796,14 +1166,14 @@ var ThreadImpl = class _ThreadImpl {
796
1166
  * Uses lazy adapter resolution from Chat.getSingleton().
797
1167
  * Requires chat.registerSingleton() to have been called.
798
1168
  */
799
- static [WORKFLOW_DESERIALIZE2](data) {
1169
+ static [WORKFLOW_DESERIALIZE3](data) {
800
1170
  return _ThreadImpl.fromJSON(data);
801
1171
  }
802
1172
  createSentMessage(messageId, postable, threadIdOverride) {
803
1173
  const adapter = this.adapter;
804
1174
  const threadId = threadIdOverride || this.id;
805
1175
  const self = this;
806
- const { plainText, formatted, attachments } = extractMessageContent(postable);
1176
+ const { plainText, formatted, attachments } = extractMessageContent2(postable);
807
1177
  const sentMessage = {
808
1178
  id: messageId,
809
1179
  threadId,
@@ -891,7 +1261,7 @@ var ThreadImpl = class _ThreadImpl {
891
1261
  };
892
1262
  }
893
1263
  };
894
- function extractMessageContent(message) {
1264
+ function extractMessageContent2(message) {
895
1265
  if (typeof message === "string") {
896
1266
  return {
897
1267
  plainText: message,
@@ -987,6 +1357,10 @@ var Chat = class {
987
1357
  actionHandlers = [];
988
1358
  modalSubmitHandlers = [];
989
1359
  modalCloseHandlers = [];
1360
+ slashCommandHandlers = [];
1361
+ assistantThreadStartedHandlers = [];
1362
+ assistantContextChangedHandlers = [];
1363
+ appHomeOpenedHandlers = [];
990
1364
  /** Initialization state */
991
1365
  initPromise = null;
992
1366
  initialized = false;
@@ -1201,6 +1575,36 @@ var Chat = class {
1201
1575
  this.logger.debug("Registered modal close handler", { callbackIds });
1202
1576
  }
1203
1577
  }
1578
+ onSlashCommand(commandOrHandler, handler) {
1579
+ if (typeof commandOrHandler === "function") {
1580
+ this.slashCommandHandlers.push({
1581
+ commands: [],
1582
+ handler: commandOrHandler
1583
+ });
1584
+ this.logger.debug("Registered slash command handler for all commands");
1585
+ } else if (handler) {
1586
+ const commands = Array.isArray(commandOrHandler) ? commandOrHandler : [commandOrHandler];
1587
+ const normalizedCommands = commands.map(
1588
+ (cmd) => cmd.startsWith("/") ? cmd : `/${cmd}`
1589
+ );
1590
+ this.slashCommandHandlers.push({ commands: normalizedCommands, handler });
1591
+ this.logger.debug("Registered slash command handler", {
1592
+ commands: normalizedCommands
1593
+ });
1594
+ }
1595
+ }
1596
+ onAssistantThreadStarted(handler) {
1597
+ this.assistantThreadStartedHandlers.push(handler);
1598
+ this.logger.debug("Registered assistant thread started handler");
1599
+ }
1600
+ onAssistantContextChanged(handler) {
1601
+ this.assistantContextChangedHandlers.push(handler);
1602
+ this.logger.debug("Registered assistant context changed handler");
1603
+ }
1604
+ onAppHomeOpened(handler) {
1605
+ this.appHomeOpenedHandlers.push(handler);
1606
+ this.logger.debug("Registered app home opened handler");
1607
+ }
1204
1608
  /**
1205
1609
  * Get an adapter by name with type safety.
1206
1610
  */
@@ -1234,6 +1638,9 @@ var Chat = class {
1234
1638
  if (typed._type === "chat:Thread") {
1235
1639
  return ThreadImpl.fromJSON(value);
1236
1640
  }
1641
+ if (typed._type === "chat:Channel") {
1642
+ return ChannelImpl.fromJSON(value);
1643
+ }
1237
1644
  if (typed._type === "chat:Message") {
1238
1645
  return Message.fromJSON(value);
1239
1646
  }
@@ -1291,14 +1698,12 @@ var Chat = class {
1291
1698
  }
1292
1699
  }
1293
1700
  async processModalSubmit(event, contextId, _options) {
1294
- const { relatedThread, relatedMessage } = await this.retrieveModalContext(
1295
- event.adapter.name,
1296
- contextId
1297
- );
1701
+ const { relatedThread, relatedMessage, relatedChannel } = await this.retrieveModalContext(event.adapter.name, contextId);
1298
1702
  const fullEvent = {
1299
1703
  ...event,
1300
1704
  relatedThread,
1301
- relatedMessage
1705
+ relatedMessage,
1706
+ relatedChannel
1302
1707
  };
1303
1708
  for (const { callbackIds, handler } of this.modalSubmitHandlers) {
1304
1709
  if (callbackIds.length === 0 || callbackIds.includes(event.callbackId)) {
@@ -1316,14 +1721,12 @@ var Chat = class {
1316
1721
  }
1317
1722
  processModalClose(event, contextId, options) {
1318
1723
  const task = (async () => {
1319
- const { relatedThread, relatedMessage } = await this.retrieveModalContext(
1320
- event.adapter.name,
1321
- contextId
1322
- );
1724
+ const { relatedThread, relatedMessage, relatedChannel } = await this.retrieveModalContext(event.adapter.name, contextId);
1323
1725
  const fullEvent = {
1324
1726
  ...event,
1325
1727
  relatedThread,
1326
- relatedMessage
1728
+ relatedMessage,
1729
+ relatedChannel
1327
1730
  };
1328
1731
  for (const { callbackIds, handler } of this.modalCloseHandlers) {
1329
1732
  if (callbackIds.length === 0 || callbackIds.includes(event.callbackId)) {
@@ -1340,15 +1743,153 @@ var Chat = class {
1340
1743
  options.waitUntil(task);
1341
1744
  }
1342
1745
  }
1746
+ /**
1747
+ * Process an incoming slash command from an adapter.
1748
+ * Handles waitUntil registration and error catching internally.
1749
+ */
1750
+ processSlashCommand(event, options) {
1751
+ const task = this.handleSlashCommandEvent(event).catch((err) => {
1752
+ this.logger.error("Slash command processing error", {
1753
+ error: err,
1754
+ command: event.command,
1755
+ text: event.text
1756
+ });
1757
+ });
1758
+ if (options?.waitUntil) {
1759
+ options.waitUntil(task);
1760
+ }
1761
+ }
1762
+ processAssistantThreadStarted(event, options) {
1763
+ const task = (async () => {
1764
+ for (const handler of this.assistantThreadStartedHandlers) {
1765
+ await handler(event);
1766
+ }
1767
+ })().catch((err) => {
1768
+ this.logger.error("Assistant thread started handler error", {
1769
+ error: err,
1770
+ threadId: event.threadId
1771
+ });
1772
+ });
1773
+ if (options?.waitUntil) {
1774
+ options.waitUntil(task);
1775
+ }
1776
+ }
1777
+ processAssistantContextChanged(event, options) {
1778
+ const task = (async () => {
1779
+ for (const handler of this.assistantContextChangedHandlers) {
1780
+ await handler(event);
1781
+ }
1782
+ })().catch((err) => {
1783
+ this.logger.error("Assistant context changed handler error", {
1784
+ error: err,
1785
+ threadId: event.threadId
1786
+ });
1787
+ });
1788
+ if (options?.waitUntil) {
1789
+ options.waitUntil(task);
1790
+ }
1791
+ }
1792
+ processAppHomeOpened(event, options) {
1793
+ const task = (async () => {
1794
+ for (const handler of this.appHomeOpenedHandlers) {
1795
+ await handler(event);
1796
+ }
1797
+ })().catch((err) => {
1798
+ this.logger.error("App home opened handler error", {
1799
+ error: err,
1800
+ userId: event.userId
1801
+ });
1802
+ });
1803
+ if (options?.waitUntil) {
1804
+ options.waitUntil(task);
1805
+ }
1806
+ }
1807
+ /**
1808
+ * Handle a slash command event internally.
1809
+ */
1810
+ async handleSlashCommandEvent(event) {
1811
+ this.logger.debug("Incoming slash command", {
1812
+ adapter: event.adapter.name,
1813
+ command: event.command,
1814
+ text: event.text,
1815
+ user: event.user.userName
1816
+ });
1817
+ if (event.user.isMe) {
1818
+ this.logger.debug("Skipping slash command from self", {
1819
+ command: event.command
1820
+ });
1821
+ return;
1822
+ }
1823
+ const channel = new ChannelImpl({
1824
+ id: event.channelId,
1825
+ adapter: event.adapter,
1826
+ stateAdapter: this._stateAdapter
1827
+ });
1828
+ const fullEvent = {
1829
+ ...event,
1830
+ channel,
1831
+ openModal: async (modal) => {
1832
+ if (!event.triggerId) {
1833
+ this.logger.warn("Cannot open modal: no triggerId available");
1834
+ return void 0;
1835
+ }
1836
+ if (!event.adapter.openModal) {
1837
+ this.logger.warn(
1838
+ `Cannot open modal: ${event.adapter.name} does not support modals`
1839
+ );
1840
+ return void 0;
1841
+ }
1842
+ let modalElement = modal;
1843
+ if (isJSX(modal)) {
1844
+ const converted = toModalElement(modal);
1845
+ if (!converted) {
1846
+ throw new Error("Invalid JSX element: must be a Modal element");
1847
+ }
1848
+ modalElement = converted;
1849
+ }
1850
+ const contextId = crypto.randomUUID();
1851
+ this.storeModalContext(
1852
+ event.adapter.name,
1853
+ contextId,
1854
+ void 0,
1855
+ void 0,
1856
+ channel
1857
+ );
1858
+ return event.adapter.openModal(
1859
+ event.triggerId,
1860
+ modalElement,
1861
+ contextId
1862
+ );
1863
+ }
1864
+ };
1865
+ this.logger.debug("Checking slash command handlers", {
1866
+ handlerCount: this.slashCommandHandlers.length,
1867
+ command: event.command
1868
+ });
1869
+ for (const { commands, handler } of this.slashCommandHandlers) {
1870
+ if (commands.length === 0) {
1871
+ this.logger.debug("Running catch-all slash command handler");
1872
+ await handler(fullEvent);
1873
+ continue;
1874
+ }
1875
+ if (commands.includes(event.command)) {
1876
+ this.logger.debug("Running matched slash command handler", {
1877
+ command: event.command
1878
+ });
1879
+ await handler(fullEvent);
1880
+ }
1881
+ }
1882
+ }
1343
1883
  /**
1344
1884
  * Store modal context server-side with a context ID.
1345
- * Called when opening a modal to preserve thread/message for the submit handler.
1885
+ * Called when opening a modal to preserve thread/message/channel for the submit handler.
1346
1886
  */
1347
- storeModalContext(adapterName, contextId, thread, message) {
1887
+ storeModalContext(adapterName, contextId, thread, message, channel) {
1348
1888
  const key = `modal-context:${adapterName}:${contextId}`;
1349
1889
  const context = {
1350
- thread: thread.toJSON(),
1351
- message: message?.toJSON()
1890
+ thread: thread?.toJSON(),
1891
+ message: message?.toJSON(),
1892
+ channel: channel?.toJSON()
1352
1893
  };
1353
1894
  this._stateAdapter.set(key, context, MODAL_CONTEXT_TTL_MS).catch((err) => {
1354
1895
  this.logger.error("Failed to store modal context", {
@@ -1359,25 +1900,40 @@ var Chat = class {
1359
1900
  }
1360
1901
  /**
1361
1902
  * Retrieve and delete modal context from server-side storage.
1362
- * Called when processing modal submit/close to reconstruct thread/message.
1903
+ * Called when processing modal submit/close to reconstruct thread/message/channel.
1363
1904
  */
1364
1905
  async retrieveModalContext(adapterName, contextId) {
1365
1906
  if (!contextId) {
1366
- return { relatedThread: void 0, relatedMessage: void 0 };
1907
+ return {
1908
+ relatedThread: void 0,
1909
+ relatedMessage: void 0,
1910
+ relatedChannel: void 0
1911
+ };
1367
1912
  }
1368
1913
  const key = `modal-context:${adapterName}:${contextId}`;
1369
1914
  const stored = await this._stateAdapter.get(key);
1370
1915
  if (!stored) {
1371
- return { relatedThread: void 0, relatedMessage: void 0 };
1916
+ return {
1917
+ relatedThread: void 0,
1918
+ relatedMessage: void 0,
1919
+ relatedChannel: void 0
1920
+ };
1372
1921
  }
1373
1922
  const adapter = this.adapters.get(adapterName);
1374
- const thread = ThreadImpl.fromJSON(stored.thread, adapter);
1923
+ let relatedThread;
1924
+ if (stored.thread) {
1925
+ relatedThread = ThreadImpl.fromJSON(stored.thread, adapter);
1926
+ }
1375
1927
  let relatedMessage;
1376
- if (stored.message) {
1928
+ if (stored.message && relatedThread) {
1377
1929
  const message = Message.fromJSON(stored.message);
1378
- relatedMessage = thread.createSentMessageFromMessage(message);
1930
+ relatedMessage = relatedThread.createSentMessageFromMessage(message);
1931
+ }
1932
+ let relatedChannel;
1933
+ if (stored.channel) {
1934
+ relatedChannel = ChannelImpl.fromJSON(stored.channel, adapter);
1379
1935
  }
1380
- return { relatedThread: thread, relatedMessage };
1936
+ return { relatedThread, relatedMessage, relatedChannel };
1381
1937
  }
1382
1938
  /**
1383
1939
  * Handle an action event internally.
@@ -1408,12 +1964,12 @@ var Chat = class {
1408
1964
  metadata: { dateSent: /* @__PURE__ */ new Date(), edited: false },
1409
1965
  attachments: []
1410
1966
  }) : {};
1411
- const thread = await this.createThread(
1967
+ const thread = event.threadId ? await this.createThread(
1412
1968
  event.adapter,
1413
1969
  event.threadId,
1414
1970
  messageForThread,
1415
1971
  isSubscribed
1416
- );
1972
+ ) : null;
1417
1973
  const fullEvent = {
1418
1974
  ...event,
1419
1975
  thread,
@@ -1455,11 +2011,13 @@ var Chat = class {
1455
2011
  }
1456
2012
  }
1457
2013
  const contextId = crypto.randomUUID();
2014
+ const channel = thread.channel;
1458
2015
  this.storeModalContext(
1459
2016
  event.adapter.name,
1460
2017
  contextId,
1461
2018
  thread,
1462
- message
2019
+ message,
2020
+ channel
1463
2021
  );
1464
2022
  return event.adapter.openModal(
1465
2023
  event.triggerId,
@@ -1601,6 +2159,53 @@ var Chat = class {
1601
2159
  const threadId = await adapter.openDM(userId);
1602
2160
  return this.createThread(adapter, threadId, {}, false);
1603
2161
  }
2162
+ /**
2163
+ * Get a Channel by its channel ID.
2164
+ *
2165
+ * The adapter is automatically inferred from the channel ID prefix.
2166
+ *
2167
+ * @param channelId - Channel ID (e.g., "slack:C123ABC", "gchat:spaces/ABC123")
2168
+ * @returns A Channel that can be used to list threads, post messages, iterate messages, etc.
2169
+ *
2170
+ * @example
2171
+ * ```typescript
2172
+ * const channel = chat.channel("slack:C123ABC");
2173
+ *
2174
+ * // Iterate messages newest first
2175
+ * for await (const msg of channel.messages) {
2176
+ * console.log(msg.text);
2177
+ * }
2178
+ *
2179
+ * // List threads
2180
+ * for await (const t of channel.threads()) {
2181
+ * console.log(t.rootMessage.text, t.replyCount);
2182
+ * }
2183
+ *
2184
+ * // Post to channel
2185
+ * await channel.post("Hello channel!");
2186
+ * ```
2187
+ */
2188
+ channel(channelId) {
2189
+ const adapterName = channelId.split(":")[0];
2190
+ if (!adapterName) {
2191
+ throw new ChatError(
2192
+ `Invalid channel ID: ${channelId}`,
2193
+ "INVALID_CHANNEL_ID"
2194
+ );
2195
+ }
2196
+ const adapter = this.adapters.get(adapterName);
2197
+ if (!adapter) {
2198
+ throw new ChatError(
2199
+ `Adapter "${adapterName}" not found for channel ID "${channelId}"`,
2200
+ "ADAPTER_NOT_FOUND"
2201
+ );
2202
+ }
2203
+ return new ChannelImpl({
2204
+ id: channelId,
2205
+ adapter,
2206
+ stateAdapter: this._stateAdapter
2207
+ });
2208
+ }
1604
2209
  /**
1605
2210
  * Infer which adapter to use based on the userId format.
1606
2211
  */
@@ -2206,6 +2811,7 @@ export {
2206
2811
  Button2 as Button,
2207
2812
  Card2 as Card,
2208
2813
  CardText2 as CardText,
2814
+ ChannelImpl,
2209
2815
  Chat,
2210
2816
  ChatError,
2211
2817
  ConsoleLogger,
@@ -2233,6 +2839,7 @@ export {
2233
2839
  convertEmojiPlaceholders,
2234
2840
  createEmoji,
2235
2841
  defaultEmojiResolver,
2842
+ deriveChannelId,
2236
2843
  emoji,
2237
2844
  emphasis,
2238
2845
  fromReactElement2 as fromReactElement,