chat 4.24.0 → 4.25.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.d.ts CHANGED
@@ -95,7 +95,9 @@ declare class ChannelImpl<TState = Record<string, unknown>> implements Channel<T
95
95
  */
96
96
  threads(): AsyncIterable<ThreadSummary>;
97
97
  fetchMetadata(): Promise<ChannelInfo>;
98
- post(message: string | PostableMessage | ChatElement): Promise<SentMessage>;
98
+ post<T extends PostableObject>(message: T): Promise<T>;
99
+ post(message: string | AdapterPostableMessage | AsyncIterable<string> | ChatElement): Promise<SentMessage>;
100
+ private handlePostableObject;
99
101
  private postSingleMessage;
100
102
  postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>;
101
103
  schedule(message: AdapterPostableMessage | ChatElement, options: {
@@ -141,6 +143,43 @@ declare class ConsoleLogger implements Logger {
141
143
  error(message: string, ...args: unknown[]): void;
142
144
  }
143
145
 
146
+ /**
147
+ * Context provided to a PostableObject after it has been posted.
148
+ */
149
+ interface PostableObjectContext {
150
+ adapter: Adapter;
151
+ logger?: Logger;
152
+ messageId: string;
153
+ threadId: string;
154
+ }
155
+ /**
156
+ * Base interface for objects that can be posted to threads/channels.
157
+ * Examples: Plan, Poll, etc.
158
+ *
159
+ * @template TData - The data type returned by getPostData()
160
+ */
161
+ interface PostableObject<TData = unknown> {
162
+ /** Symbol identifying this as a postable object */
163
+ readonly $$typeof: symbol;
164
+ /**
165
+ * Get a fallback text representation for adapters that don't support this object type.
166
+ * This should return a human-readable string representation.
167
+ */
168
+ getFallbackText(): string;
169
+ /** Get the data to send to the adapter */
170
+ getPostData(): TData;
171
+ /** Check if the adapter supports this object type */
172
+ isSupported(adapter: Adapter): boolean;
173
+ /** The kind of object - used by adapters to dispatch */
174
+ readonly kind: string;
175
+ /** Called after successful posting to bind the object to the thread */
176
+ onPosted(context: PostableObjectContext): void;
177
+ }
178
+ /**
179
+ * Type guard to check if a value is a PostableObject.
180
+ */
181
+ declare function isPostableObject(value: unknown): value is PostableObject;
182
+
144
183
  /**
145
184
  * Serialized thread data for passing to external systems (e.g., workflow engines).
146
185
  */
@@ -252,7 +291,9 @@ declare class ThreadImpl<TState = Record<string, unknown>> implements Thread<TSt
252
291
  isSubscribed(): Promise<boolean>;
253
292
  subscribe(): Promise<void>;
254
293
  unsubscribe(): Promise<void>;
255
- post(message: string | PostableMessage | ChatElement): Promise<SentMessage>;
294
+ post<T extends PostableObject>(message: T): Promise<T>;
295
+ post(message: string | AdapterPostableMessage | AsyncIterable<string> | ChatElement): Promise<SentMessage>;
296
+ private handlePostableObject;
256
297
  postEphemeral(user: string | Author, message: AdapterPostableMessage | ChatElement, options: PostEphemeralOptions): Promise<EphemeralMessage | null>;
257
298
  schedule(message: AdapterPostableMessage | ChatElement, options: {
258
299
  postAt: Date;
@@ -337,6 +378,85 @@ declare class NotImplementedError extends ChatError {
337
378
  constructor(message: string, feature?: string, cause?: unknown);
338
379
  }
339
380
 
381
+ type PlanTaskStatus = "pending" | "in_progress" | "complete" | "error";
382
+ interface PlanTask {
383
+ id: string;
384
+ status: PlanTaskStatus;
385
+ title: string;
386
+ }
387
+ interface PlanModel {
388
+ tasks: PlanModelTask[];
389
+ title: string;
390
+ }
391
+ interface PlanModelTask {
392
+ details?: PlanContent;
393
+ id: string;
394
+ output?: PlanContent;
395
+ status: PlanTaskStatus;
396
+ title: string;
397
+ }
398
+ type PlanContent = string | string[] | {
399
+ markdown: string;
400
+ } | {
401
+ ast: Root;
402
+ };
403
+ interface StartPlanOptions {
404
+ /** Initial plan title and first task title */
405
+ initialMessage: PlanContent;
406
+ }
407
+ interface AddTaskOptions {
408
+ /** Task details/substeps. */
409
+ children?: PlanContent;
410
+ title: PlanContent;
411
+ }
412
+ type UpdateTaskInput = PlanContent | {
413
+ /** Task output/results. */
414
+ output?: PlanContent;
415
+ /** Optional status override. */
416
+ status?: PlanTaskStatus;
417
+ };
418
+ interface CompletePlanOptions {
419
+ /** Final plan title shown when completed */
420
+ completeMessage: PlanContent;
421
+ }
422
+ /**
423
+ * A Plan represents a task list that can be posted to a thread.
424
+ *
425
+ * Create a plan with `new Plan({ initialMessage: "..." })` and post it with `thread.post(plan)`.
426
+ * After posting, use methods like `addTask()`, `updateTask()`, and `complete()` to update it.
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const plan = new Plan({ initialMessage: "Starting task..." });
431
+ * await thread.post(plan);
432
+ * await plan.addTask({ title: "Fetch data" });
433
+ * await plan.updateTask("Got 42 results");
434
+ * await plan.complete({ completeMessage: "Done!" });
435
+ * ```
436
+ */
437
+ declare class Plan implements PostableObject<PlanModel> {
438
+ readonly $$typeof: symbol;
439
+ readonly kind = "plan";
440
+ private _model;
441
+ private _bound;
442
+ constructor(options: StartPlanOptions);
443
+ isSupported(adapter: Adapter): boolean;
444
+ getPostData(): PlanModel;
445
+ getFallbackText(): string;
446
+ onPosted(context: PostableObjectContext): void;
447
+ get id(): string;
448
+ get threadId(): string;
449
+ get title(): string;
450
+ get tasks(): readonly PlanTask[];
451
+ get currentTask(): PlanTask | null;
452
+ addTask(options: AddTaskOptions): Promise<PlanTask | null>;
453
+ updateTask(update?: UpdateTaskInput): Promise<PlanTask | null>;
454
+ reset(options: StartPlanOptions): Promise<PlanTask | null>;
455
+ complete(options: CompletePlanOptions): Promise<void>;
456
+ private canMutate;
457
+ private enqueueEdit;
458
+ }
459
+
340
460
  /**
341
461
  * Represents the visibility scope of a channel.
342
462
  *
@@ -488,6 +608,16 @@ interface Adapter<TThreadId = unknown, TRawMessage = unknown> {
488
608
  disconnect?(): Promise<void>;
489
609
  /** Edit an existing message */
490
610
  editMessage(threadId: string, messageId: string, message: AdapterPostableMessage): Promise<RawMessage<TRawMessage>>;
611
+ /**
612
+ * Edit a previously posted object (Plan, Poll, etc.).
613
+ * If not implemented, object updates will throw PlanNotSupportedError.
614
+ *
615
+ * @param threadId - The thread containing the message
616
+ * @param messageId - The message ID to edit
617
+ * @param kind - The object kind (e.g., "plan")
618
+ * @param data - The object data (type depends on kind)
619
+ */
620
+ editObject?(threadId: string, messageId: string, kind: string, data: unknown): Promise<RawMessage<TRawMessage>>;
491
621
  /** Encode platform-specific data into a thread ID string */
492
622
  encodeThreadId(platformData: TThreadId): string;
493
623
  /**
@@ -633,6 +763,15 @@ interface Adapter<TThreadId = unknown, TRawMessage = unknown> {
633
763
  postEphemeral?(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage<TRawMessage>>;
634
764
  /** Post a message to a thread */
635
765
  postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<TRawMessage>>;
766
+ /**
767
+ * Post a special object (Plan, Poll, etc.) as a single message.
768
+ * If not implemented, posting such objects will throw PlanNotSupportedError.
769
+ *
770
+ * @param threadId - The thread to post to
771
+ * @param kind - The object kind (e.g., "plan")
772
+ * @param data - The object data (type depends on kind)
773
+ */
774
+ postObject?(threadId: string, kind: string, data: unknown): Promise<RawMessage<TRawMessage>>;
636
775
  /** Remove a reaction from a message */
637
776
  removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
638
777
  /** Render formatted content to platform-specific string */
@@ -920,6 +1059,7 @@ interface Postable<TState = Record<string, unknown>, TRawMessage = unknown> {
920
1059
  /**
921
1060
  * Post a message.
922
1061
  */
1062
+ post<T extends PostableObject>(message: T): Promise<T>;
923
1063
  post(message: string | PostableMessage | ChatElement): Promise<SentMessage<TRawMessage>>;
924
1064
  /**
925
1065
  * Post an ephemeral message visible only to a specific user.
@@ -1097,8 +1237,15 @@ interface Thread<TState = Record<string, unknown>, TRawMessage = unknown> extend
1097
1237
  * // Stream from AI SDK
1098
1238
  * const result = await agent.stream({ prompt: message.text });
1099
1239
  * await thread.post(result.textStream);
1240
+ *
1241
+ * // Plan with live updates
1242
+ * const plan = new Plan({ initialMessage: "Working..." });
1243
+ * await thread.post(plan);
1244
+ * await plan.addTask({ title: "Step 1" });
1245
+ * await plan.complete({ completeMessage: "Done!" });
1100
1246
  * ```
1101
1247
  */
1248
+ post<T extends PostableObject>(message: T): Promise<T>;
1102
1249
  post(message: string | PostableMessage | ChatElement): Promise<SentMessage<TRawMessage>>;
1103
1250
  /**
1104
1251
  * Post an ephemeral message visible only to a specific user.
@@ -1171,6 +1318,7 @@ interface Thread<TState = Record<string, unknown>, TRawMessage = unknown> extend
1171
1318
  */
1172
1319
  unsubscribe(): Promise<void>;
1173
1320
  }
1321
+
1174
1322
  interface ThreadInfo {
1175
1323
  channelId: string;
1176
1324
  channelName?: string;
@@ -1358,7 +1506,7 @@ type AdapterPostableMessage = string | PostableRaw | PostableMarkdown | Postable
1358
1506
  * - `AsyncIterable<string>` - Streaming text (e.g., from AI SDK's textStream)
1359
1507
  * - `AsyncIterable<string | StreamEvent>` - AI SDK fullStream (auto-detected, extracts text with step separators)
1360
1508
  */
1361
- type PostableMessage = AdapterPostableMessage | AsyncIterable<string | StreamChunk | StreamEvent>;
1509
+ type PostableMessage = AdapterPostableMessage | AsyncIterable<string | StreamChunk | StreamEvent> | PostableObject;
1362
1510
  /**
1363
1511
  * Duck-typed stream event compatible with AI SDK's `fullStream`.
1364
1512
  * - `text-delta` events are extracted as text output.
@@ -2733,6 +2881,13 @@ declare class Chat<TAdapters extends Record<string, Adapter> = Record<string, Ad
2733
2881
  */
2734
2882
  declare function fromFullStream(stream: AsyncIterable<unknown>): AsyncIterable<string | StreamChunk>;
2735
2883
 
2884
+ interface StreamingMarkdownRendererOptions {
2885
+ /**
2886
+ * Wrap confirmed table blocks in code fences for append-only consumers that
2887
+ * cannot render markdown tables while a stream is in flight.
2888
+ */
2889
+ wrapTablesForAppend?: boolean;
2890
+ }
2736
2891
  /**
2737
2892
  * A streaming markdown renderer that buffers potential table headers
2738
2893
  * until confirmed by a separator line, preventing tables from flashing
@@ -2750,6 +2905,8 @@ declare class StreamingMarkdownRenderer {
2750
2905
  private fenceToggles;
2751
2906
  /** Incomplete trailing line buffer for incremental fence tracking. */
2752
2907
  private incompleteLine;
2908
+ private readonly options;
2909
+ constructor(options?: StreamingMarkdownRendererOptions);
2753
2910
  /** Append a chunk from the LLM stream. */
2754
2911
  push(chunk: string): void;
2755
2912
  /** O(1) check if accumulated text is inside an unclosed code fence. */
@@ -2766,9 +2923,10 @@ declare class StreamingMarkdownRenderer {
2766
2923
  * Get text safe for append-only streaming (e.g. Slack native streaming).
2767
2924
  *
2768
2925
  * - Holds back unconfirmed table headers until separator arrives.
2769
- * - Wraps confirmed tables in code fences so pipes render as literal
2770
- * text (not broken mrkdwn). The code fence is left OPEN while
2771
- * the table is still streaming, keeping output monotonic for deltas.
2926
+ * - Optionally wraps confirmed tables in code fences so pipes render as
2927
+ * literal text on append-only surfaces that lack native table support.
2928
+ * The code fence is left OPEN while the table is still streaming,
2929
+ * keeping output monotonic for deltas.
2772
2930
  * - Holds back unclosed inline markers (**, *, ~~, `, [).
2773
2931
  * - The final editMessage replaces everything with properly formatted text.
2774
2932
  */
@@ -2777,6 +2935,7 @@ declare class StreamingMarkdownRenderer {
2777
2935
  getText(): string;
2778
2936
  /** Signal stream end. Flushes held-back lines. Returns final render. */
2779
2937
  finish(): string;
2938
+ private formatAppendOnlyText;
2780
2939
  }
2781
2940
 
2782
2941
  /**
@@ -3222,4 +3381,4 @@ declare const Select: SelectComponent;
3222
3381
  declare const SelectOption: SelectOptionComponent;
3223
3382
  declare const TextInput: TextInputComponent;
3224
3383
 
3225
- export { type ActionEvent, type ActionHandler, Actions, ActionsComponent, type Adapter, type AdapterPostableMessage, type AiAssistantMessage, type AiFilePart, type AiImagePart, type AiMessage, type AiMessagePart, type AiTextPart, type AiUserMessage, type AppHomeOpenedEvent, type AppHomeOpenedHandler, type AssistantContextChangedEvent, type AssistantContextChangedHandler, type AssistantThreadStartedEvent, type AssistantThreadStartedHandler, type Attachment, type Author, BaseFormatConverter, Button, ButtonComponent, Card, CardChild, CardComponent, CardElement, CardLink, CardLinkComponent, CardText, type Channel, ChannelImpl, type ChannelInfo, type ChannelVisibility, Chat, type ChatConfig, ChatElement, ChatError, type ChatInstance, type ConcurrencyConfig, type ConcurrencyStrategy, ConsoleLogger, type CustomEmojiMap, DEFAULT_EMOJI_MAP, type DirectMessageHandler, Divider, DividerComponent, type Emoji, type EmojiFormats, type EmojiMapConfig, EmojiResolver, type EmojiValue, type EphemeralMessage, type FetchDirection, type FetchOptions, type FetchResult, Field, FieldComponent, Fields, FieldsComponent, type FileUpload, type FormatConverter, type FormattedContent, Image, ImageComponent, LinkButton, LinkButtonComponent, type LinkPreview, type ListThreadsOptions, type ListThreadsResult, type Lock, LockError, type LockScope, type LockScopeContext, type LogLevel, type Logger, type MarkdownConverter, type MarkdownTextChunk, type MemberJoinedChannelEvent, type MemberJoinedChannelHandler, type MentionHandler, Message, type MessageContext, type MessageData, type MessageHandler, MessageHistoryCache, type MessageHistoryConfig, type MessageMetadata, Modal, type ModalCloseEvent, type ModalCloseHandler, type ModalCloseResponse, ModalComponent, ModalElement, type ModalErrorsResponse, type ModalPushResponse, type ModalResponse, type ModalSubmitEvent, type ModalSubmitHandler, type ModalUpdateResponse, NotImplementedError, type PlanUpdateChunk, type PostEphemeralOptions, type Postable, type PostableAst, type PostableCard, type PostableMarkdown, type PostableMessage, type PostableRaw, type QueueEntry, RadioSelect, RadioSelectComponent, RateLimitError, type RawMessage, type ReactionEvent, type ReactionHandler, type ScheduledMessage, Section, SectionComponent, Select, SelectComponent, SelectOption, SelectOptionComponent, type SentMessage, type SerializedChannel, type SerializedMessage, type SerializedThread, type SlashCommandEvent, type SlashCommandHandler, type StateAdapter, type StreamChunk, type StreamEvent, type StreamOptions, StreamingMarkdownRenderer, type SubscribedMessageHandler, THREAD_STATE_TTL_MS, Table, type TaskUpdateChunk, TextComponent, TextInput, TextInputComponent, type Thread, ThreadImpl, type ThreadInfo, type ThreadSummary, type WebhookOptions, type WellKnownEmoji, blockquote, cardChildToFallbackText, codeBlock, convertEmojiPlaceholders, createEmoji, defaultEmojiResolver, deriveChannelId, emoji, emphasis, fromFullStream, fromReactElement, fromReactModalElement, getEmoji, getNodeChildren, getNodeValue, inlineCode, isBlockquoteNode, isCardElement, isCodeNode, isDeleteNode, isEmphasisNode, isInlineCodeNode, isJSX, isLinkNode, isListItemNode, isListNode, isModalElement, isParagraphNode, isStrongNode, isTableCellNode, isTableNode, isTableRowNode, isTextNode, link, markdownToPlainText, paragraph, parseMarkdown, root, strikethrough, stringifyMarkdown, strong, tableElementToAscii, tableToAscii, text, toAiMessages, toCardElement, toModalElement, toPlainText, walkAst };
3384
+ export { type ActionEvent, type ActionHandler, Actions, ActionsComponent, type Adapter, type AdapterPostableMessage, type AddTaskOptions, type AiAssistantMessage, type AiFilePart, type AiImagePart, type AiMessage, type AiMessagePart, type AiTextPart, type AiUserMessage, type AppHomeOpenedEvent, type AppHomeOpenedHandler, type AssistantContextChangedEvent, type AssistantContextChangedHandler, type AssistantThreadStartedEvent, type AssistantThreadStartedHandler, type Attachment, type Author, BaseFormatConverter, Button, ButtonComponent, Card, CardChild, CardComponent, CardElement, CardLink, CardLinkComponent, CardText, type Channel, ChannelImpl, type ChannelInfo, type ChannelVisibility, Chat, type ChatConfig, ChatElement, ChatError, type ChatInstance, type CompletePlanOptions, type ConcurrencyConfig, type ConcurrencyStrategy, ConsoleLogger, type CustomEmojiMap, DEFAULT_EMOJI_MAP, type DirectMessageHandler, Divider, DividerComponent, type Emoji, type EmojiFormats, type EmojiMapConfig, EmojiResolver, type EmojiValue, type EphemeralMessage, type FetchDirection, type FetchOptions, type FetchResult, Field, FieldComponent, Fields, FieldsComponent, type FileUpload, type FormatConverter, type FormattedContent, Image, ImageComponent, LinkButton, LinkButtonComponent, type LinkPreview, type ListThreadsOptions, type ListThreadsResult, type Lock, LockError, type LockScope, type LockScopeContext, type LogLevel, type Logger, type MarkdownConverter, type MarkdownTextChunk, type MemberJoinedChannelEvent, type MemberJoinedChannelHandler, type MentionHandler, Message, type MessageContext, type MessageData, type MessageHandler, MessageHistoryCache, type MessageHistoryConfig, type MessageMetadata, Modal, type ModalCloseEvent, type ModalCloseHandler, type ModalCloseResponse, ModalComponent, ModalElement, type ModalErrorsResponse, type ModalPushResponse, type ModalResponse, type ModalSubmitEvent, type ModalSubmitHandler, type ModalUpdateResponse, NotImplementedError, Plan, type PlanContent, type PlanModel, type PlanModelTask, type PlanTask, type PlanTaskStatus, type PlanUpdateChunk, type PostEphemeralOptions, type Postable, type PostableAst, type PostableCard, type PostableMarkdown, type PostableMessage, type PostableObject, type PostableObjectContext, type PostableRaw, type QueueEntry, RadioSelect, RadioSelectComponent, RateLimitError, type RawMessage, type ReactionEvent, type ReactionHandler, type ScheduledMessage, Section, SectionComponent, Select, SelectComponent, SelectOption, SelectOptionComponent, type SentMessage, type SerializedChannel, type SerializedMessage, type SerializedThread, type SlashCommandEvent, type SlashCommandHandler, type StartPlanOptions, type StateAdapter, type StreamChunk, type StreamEvent, type StreamOptions, StreamingMarkdownRenderer, type SubscribedMessageHandler, THREAD_STATE_TTL_MS, Table, type TaskUpdateChunk, TextComponent, TextInput, TextInputComponent, type Thread, ThreadImpl, type ThreadInfo, type ThreadSummary, type UpdateTaskInput, type WebhookOptions, type WellKnownEmoji, blockquote, cardChildToFallbackText, codeBlock, convertEmojiPlaceholders, createEmoji, defaultEmojiResolver, deriveChannelId, emoji, emphasis, fromFullStream, fromReactElement, fromReactModalElement, getEmoji, getNodeChildren, getNodeValue, inlineCode, isBlockquoteNode, isCardElement, isCodeNode, isDeleteNode, isEmphasisNode, isInlineCodeNode, isJSX, isLinkNode, isListItemNode, isListNode, isModalElement, isParagraphNode, isPostableObject, isStrongNode, isTableCellNode, isTableNode, isTableRowNode, isTextNode, link, markdownToPlainText, paragraph, parseMarkdown, root, strikethrough, stringifyMarkdown, strong, tableElementToAscii, tableToAscii, text, toAiMessages, toCardElement, toModalElement, toPlainText, walkAst };
package/dist/index.js CHANGED
@@ -380,6 +380,27 @@ var Message = class _Message {
380
380
  }
381
381
  };
382
382
 
383
+ // src/postable-object.ts
384
+ var POSTABLE_OBJECT = /* @__PURE__ */ Symbol.for("chat.postable");
385
+ function isPostableObject(value) {
386
+ return typeof value === "object" && value !== null && value.$$typeof === POSTABLE_OBJECT;
387
+ }
388
+ async function postPostableObject(obj, adapter, threadId, postFn, logger) {
389
+ const context = (raw) => ({
390
+ adapter,
391
+ logger,
392
+ messageId: raw.id,
393
+ threadId: raw.threadId ?? threadId
394
+ });
395
+ if (obj.isSupported(adapter) && adapter.postObject) {
396
+ const raw = await adapter.postObject(threadId, obj.kind, obj.getPostData());
397
+ obj.onPosted(context(raw));
398
+ } else {
399
+ const raw = await postFn(threadId, obj.getFallbackText());
400
+ obj.onPosted(context(raw));
401
+ }
402
+ }
403
+
383
404
  // src/errors.ts
384
405
  var ChatError = class extends Error {
385
406
  code;
@@ -605,6 +626,10 @@ var ChannelImpl = class _ChannelImpl {
605
626
  };
606
627
  }
607
628
  async post(message) {
629
+ if (isPostableObject(message)) {
630
+ await this.handlePostableObject(message);
631
+ return message;
632
+ }
608
633
  if (isAsyncIterable(message)) {
609
634
  let accumulated = "";
610
635
  for await (const chunk of fromFullStream(message)) {
@@ -624,6 +649,14 @@ var ChannelImpl = class _ChannelImpl {
624
649
  }
625
650
  return this.postSingleMessage(postable);
626
651
  }
652
+ async handlePostableObject(obj) {
653
+ await postPostableObject(
654
+ obj,
655
+ this.adapter,
656
+ this.id,
657
+ (threadId, message) => this.adapter.postChannelMessage ? this.adapter.postChannelMessage(threadId, message) : this.adapter.postMessage(threadId, message)
658
+ );
659
+ }
627
660
  async postSingleMessage(postable) {
628
661
  const rawMessage = this.adapter.postChannelMessage ? await this.adapter.postChannelMessage(this.id, postable) : await this.adapter.postMessage(this.id, postable);
629
662
  const sent = this.createSentMessage(
@@ -877,6 +910,12 @@ var StreamingMarkdownRenderer = class {
877
910
  fenceToggles = 0;
878
911
  /** Incomplete trailing line buffer for incremental fence tracking. */
879
912
  incompleteLine = "";
913
+ options;
914
+ constructor(options = {}) {
915
+ this.options = {
916
+ wrapTablesForAppend: options.wrapTablesForAppend ?? true
917
+ };
918
+ }
880
919
  /** Append a chunk from the LLM stream. */
881
920
  push(chunk) {
882
921
  this.accumulated += chunk;
@@ -928,30 +967,31 @@ var StreamingMarkdownRenderer = class {
928
967
  * Get text safe for append-only streaming (e.g. Slack native streaming).
929
968
  *
930
969
  * - Holds back unconfirmed table headers until separator arrives.
931
- * - Wraps confirmed tables in code fences so pipes render as literal
932
- * text (not broken mrkdwn). The code fence is left OPEN while
933
- * the table is still streaming, keeping output monotonic for deltas.
970
+ * - Optionally wraps confirmed tables in code fences so pipes render as
971
+ * literal text on append-only surfaces that lack native table support.
972
+ * The code fence is left OPEN while the table is still streaming,
973
+ * keeping output monotonic for deltas.
934
974
  * - Holds back unclosed inline markers (**, *, ~~, `, [).
935
975
  * - The final editMessage replaces everything with properly formatted text.
936
976
  */
937
977
  getCommittableText() {
938
978
  if (this.finished) {
939
- return wrapTablesForAppend(this.accumulated, true);
979
+ return this.formatAppendOnlyText(this.accumulated, true);
940
980
  }
941
981
  let text2 = this.accumulated;
942
982
  if (text2.length > 0 && !text2.endsWith("\n")) {
943
983
  const lastNewline = text2.lastIndexOf("\n");
944
984
  const withoutIncompleteLine = lastNewline >= 0 ? text2.slice(0, lastNewline + 1) : "";
945
985
  if (isInsideCodeFence(withoutIncompleteLine)) {
946
- return wrapTablesForAppend(text2);
986
+ return this.formatAppendOnlyText(text2);
947
987
  }
948
988
  text2 = withoutIncompleteLine;
949
989
  }
950
990
  if (isInsideCodeFence(text2)) {
951
- return wrapTablesForAppend(text2);
991
+ return this.formatAppendOnlyText(text2);
952
992
  }
953
993
  const committed = getCommittablePrefix(text2);
954
- const wrapped = wrapTablesForAppend(committed);
994
+ const wrapped = this.formatAppendOnlyText(committed);
955
995
  if (isInsideCodeFence(wrapped)) {
956
996
  return wrapped;
957
997
  }
@@ -967,6 +1007,12 @@ var StreamingMarkdownRenderer = class {
967
1007
  this.dirty = true;
968
1008
  return this.render();
969
1009
  }
1010
+ formatAppendOnlyText(text2, closeFences = false) {
1011
+ if (!this.options.wrapTablesForAppend) {
1012
+ return text2;
1013
+ }
1014
+ return wrapTablesForAppend(text2, closeFences);
1015
+ }
970
1016
  };
971
1017
  var INLINE_MARKER_CHARS = /* @__PURE__ */ new Set(["*", "~", "`", "["]);
972
1018
  function isClean(text2) {
@@ -1308,6 +1354,10 @@ var ThreadImpl = class _ThreadImpl {
1308
1354
  await this._stateAdapter.unsubscribe(this.id);
1309
1355
  }
1310
1356
  async post(message) {
1357
+ if (isPostableObject(message)) {
1358
+ await this.handlePostableObject(message);
1359
+ return message;
1360
+ }
1311
1361
  if (isAsyncIterable2(message)) {
1312
1362
  return this.handleStream(message);
1313
1363
  }
@@ -1330,6 +1380,15 @@ var ThreadImpl = class _ThreadImpl {
1330
1380
  }
1331
1381
  return result;
1332
1382
  }
1383
+ async handlePostableObject(obj) {
1384
+ await postPostableObject(
1385
+ obj,
1386
+ this.adapter,
1387
+ this.id,
1388
+ (threadId, message) => this.adapter.postMessage(threadId, message),
1389
+ this._logger
1390
+ );
1391
+ }
1333
1392
  async postEphemeral(user, message, options) {
1334
1393
  const { fallbackToDM } = options;
1335
1394
  const userId = typeof user === "string" ? user : user.userId;
@@ -3311,6 +3370,220 @@ var Chat = class {
3311
3370
  }
3312
3371
  };
3313
3372
 
3373
+ // src/plan.ts
3374
+ function contentToPlainText(content) {
3375
+ if (!content) {
3376
+ return "";
3377
+ }
3378
+ if (Array.isArray(content)) {
3379
+ return content.join(" ").trim();
3380
+ }
3381
+ if (typeof content === "string") {
3382
+ return content;
3383
+ }
3384
+ if ("markdown" in content) {
3385
+ return toPlainText(parseMarkdown(content.markdown));
3386
+ }
3387
+ if ("ast" in content) {
3388
+ return toPlainText(content.ast);
3389
+ }
3390
+ return "";
3391
+ }
3392
+ var Plan = class {
3393
+ $$typeof = POSTABLE_OBJECT;
3394
+ kind = "plan";
3395
+ _model;
3396
+ _bound = null;
3397
+ constructor(options) {
3398
+ const title = contentToPlainText(options.initialMessage) || "Plan";
3399
+ const firstTask = {
3400
+ id: crypto.randomUUID(),
3401
+ title,
3402
+ status: "in_progress"
3403
+ };
3404
+ this._model = { title, tasks: [firstTask] };
3405
+ }
3406
+ isSupported(adapter) {
3407
+ return !!adapter.postObject && !!adapter.editObject;
3408
+ }
3409
+ getPostData() {
3410
+ return this._model;
3411
+ }
3412
+ getFallbackText() {
3413
+ const lines = [];
3414
+ lines.push(`\u{1F4CB} ${this._model.title || "Plan"}`);
3415
+ for (const task of this._model.tasks) {
3416
+ const statusIcons = {
3417
+ complete: "\u2705",
3418
+ in_progress: "\u{1F504}",
3419
+ error: "\u274C"
3420
+ };
3421
+ const statusIcon = statusIcons[task.status] ?? "\u2B1C";
3422
+ lines.push(`${statusIcon} ${task.title}`);
3423
+ }
3424
+ return lines.join("\n");
3425
+ }
3426
+ onPosted(context) {
3427
+ this._bound = {
3428
+ adapter: context.adapter,
3429
+ fallback: !this.isSupported(context.adapter),
3430
+ logger: context.logger,
3431
+ messageId: context.messageId,
3432
+ threadId: context.threadId,
3433
+ updateChain: Promise.resolve()
3434
+ };
3435
+ }
3436
+ get id() {
3437
+ return this._bound?.messageId ?? "";
3438
+ }
3439
+ get threadId() {
3440
+ return this._bound?.threadId ?? "";
3441
+ }
3442
+ get title() {
3443
+ return this._model.title;
3444
+ }
3445
+ get tasks() {
3446
+ return this._model.tasks.map((t) => ({
3447
+ id: t.id,
3448
+ title: t.title,
3449
+ status: t.status
3450
+ }));
3451
+ }
3452
+ get currentTask() {
3453
+ let current;
3454
+ for (let i = this._model.tasks.length - 1; i >= 0; i--) {
3455
+ if (this._model.tasks[i].status === "in_progress") {
3456
+ current = this._model.tasks[i];
3457
+ break;
3458
+ }
3459
+ }
3460
+ current ??= this._model.tasks.at(-1);
3461
+ if (!current) {
3462
+ return null;
3463
+ }
3464
+ return { id: current.id, title: current.title, status: current.status };
3465
+ }
3466
+ async addTask(options) {
3467
+ if (!this.canMutate()) {
3468
+ return null;
3469
+ }
3470
+ const title = contentToPlainText(options.title) || "Task";
3471
+ for (const task of this._model.tasks) {
3472
+ if (task.status === "in_progress") {
3473
+ task.status = "complete";
3474
+ }
3475
+ }
3476
+ const nextTask = {
3477
+ id: crypto.randomUUID(),
3478
+ title,
3479
+ status: "in_progress",
3480
+ details: options.children
3481
+ };
3482
+ this._model.tasks.push(nextTask);
3483
+ this._model.title = title;
3484
+ await this.enqueueEdit();
3485
+ return { id: nextTask.id, title: nextTask.title, status: nextTask.status };
3486
+ }
3487
+ async updateTask(update) {
3488
+ if (!this.canMutate()) {
3489
+ return null;
3490
+ }
3491
+ let current;
3492
+ for (let i = this._model.tasks.length - 1; i >= 0; i--) {
3493
+ if (this._model.tasks[i].status === "in_progress") {
3494
+ current = this._model.tasks[i];
3495
+ break;
3496
+ }
3497
+ }
3498
+ current ??= this._model.tasks.at(-1);
3499
+ if (!current) {
3500
+ return null;
3501
+ }
3502
+ if (update !== void 0) {
3503
+ if (typeof update === "object" && update !== null && "output" in update) {
3504
+ if (update.output !== void 0) {
3505
+ current.output = update.output;
3506
+ }
3507
+ if (update.status) {
3508
+ current.status = update.status;
3509
+ }
3510
+ } else {
3511
+ current.output = update;
3512
+ }
3513
+ }
3514
+ await this.enqueueEdit();
3515
+ return { id: current.id, title: current.title, status: current.status };
3516
+ }
3517
+ async reset(options) {
3518
+ if (!this.canMutate()) {
3519
+ return null;
3520
+ }
3521
+ const title = contentToPlainText(options.initialMessage) || "Plan";
3522
+ const firstTask = {
3523
+ id: crypto.randomUUID(),
3524
+ title,
3525
+ status: "in_progress"
3526
+ };
3527
+ this._model = { title, tasks: [firstTask] };
3528
+ await this.enqueueEdit();
3529
+ return {
3530
+ id: firstTask.id,
3531
+ title: firstTask.title,
3532
+ status: firstTask.status
3533
+ };
3534
+ }
3535
+ async complete(options) {
3536
+ if (!this.canMutate()) {
3537
+ return;
3538
+ }
3539
+ for (const task of this._model.tasks) {
3540
+ if (task.status === "in_progress") {
3541
+ task.status = "complete";
3542
+ }
3543
+ }
3544
+ this._model.title = contentToPlainText(options.completeMessage) || this._model.title;
3545
+ await this.enqueueEdit();
3546
+ }
3547
+ canMutate() {
3548
+ return !!this._bound;
3549
+ }
3550
+ enqueueEdit() {
3551
+ if (!this._bound) {
3552
+ return Promise.resolve();
3553
+ }
3554
+ const bound = this._bound;
3555
+ const doEdit = async () => {
3556
+ if (bound.fallback) {
3557
+ await bound.adapter.editMessage(
3558
+ bound.threadId,
3559
+ bound.messageId,
3560
+ this.getFallbackText()
3561
+ );
3562
+ } else {
3563
+ const editObject = bound.adapter.editObject;
3564
+ if (!editObject) {
3565
+ return;
3566
+ }
3567
+ await editObject.call(
3568
+ bound.adapter,
3569
+ bound.threadId,
3570
+ bound.messageId,
3571
+ this.kind,
3572
+ this._model
3573
+ );
3574
+ }
3575
+ };
3576
+ const chained = bound.updateChain.then(doEdit, doEdit);
3577
+ bound.updateChain = chained.then(
3578
+ () => void 0,
3579
+ (err) => {
3580
+ bound.logger?.warn("Failed to edit plan", err);
3581
+ }
3582
+ );
3583
+ return chained;
3584
+ }
3585
+ };
3586
+
3314
3587
  // src/emoji.ts
3315
3588
  var emojiRegistry = /* @__PURE__ */ new Map();
3316
3589
  function getEmoji(name) {
@@ -3749,6 +4022,7 @@ export {
3749
4022
  MessageHistoryCache,
3750
4023
  Modal2 as Modal,
3751
4024
  NotImplementedError,
4025
+ Plan,
3752
4026
  RadioSelect2 as RadioSelect,
3753
4027
  RateLimitError,
3754
4028
  Section2 as Section,
@@ -3787,6 +4061,7 @@ export {
3787
4061
  isListNode,
3788
4062
  isModalElement2 as isModalElement,
3789
4063
  isParagraphNode,
4064
+ isPostableObject,
3790
4065
  isStrongNode,
3791
4066
  isTableCellNode,
3792
4067
  isTableNode,
@@ -87,6 +87,8 @@ After creating the app:
87
87
  1. Go to **OAuth & Permissions**, click **Install to Workspace**, and copy the **Bot User OAuth Token** (`xoxb-...`) — you'll need this as `SLACK_BOT_TOKEN`
88
88
  2. Go to **Basic Information** → **App Credentials** and copy the **Signing Secret** — you'll need this as `SLACK_SIGNING_SECRET`
89
89
 
90
+ If you're distributing the app across multiple workspaces via OAuth instead of installing it to one workspace, configure `clientId` and `clientSecret` on the Slack adapter and pass the same redirect URI used during the authorize step into `handleOAuthCallback(request, { redirectUri })` in your callback route.
91
+
90
92
  ## Configure environment variables
91
93
 
92
94
  Create a `.env.local` file in your project root:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat",
3
- "version": "4.24.0",
3
+ "version": "4.25.0",
4
4
  "description": "Unified chat abstraction for Slack, Teams, Google Chat, and Discord",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",