diff --git a/packages/types/src/chunk.ts b/packages/types/src/chunk.ts new file mode 100644 index 000000000..6acddb06b --- /dev/null +++ b/packages/types/src/chunk.ts @@ -0,0 +1,106 @@ +/** + * Base interface for streaming message chunks. + * https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming + */ +export interface Chunk { + type: string; +} + +/** + * Used for streaming text content with markdown formatting support. + * https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming + */ +export interface MarkdownTextChunk extends Chunk { + type: 'markdown_text'; + text: string; +} + +/** + * URL source for task update chunks. + */ +export interface URLSource { + type: 'url'; + url: string; + text: string; + icon_url?: string; +} + +/** + * An updated title of plans for task and tool calls. + * https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming + */ +export interface PlanUpdateChunk extends Chunk { + type: 'plan_update'; + title: string; +} + +/** + * Used for displaying tool execution progress in a timeline-style UI. + * https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming + */ +export interface TaskUpdateChunk extends Chunk { + type: 'task_update'; + id: string; + title: string; + status: 'pending' | 'in_progress' | 'complete' | 'error'; + details?: string; + output?: string; + sources?: URLSource[]; +} + +/** + * Union type of all possible chunk types + */ +export type AnyChunk = MarkdownTextChunk | PlanUpdateChunk | TaskUpdateChunk; + +/** + * Parse a chunk object and return the appropriate typed chunk. + * Returns null if the chunk is invalid or unknown. + */ +export function parseChunk(chunk: unknown): AnyChunk | null { + if (!chunk || typeof chunk !== 'object') { + return null; + } + + const chunkObj = chunk as Record; + + if (!('type' in chunkObj) || typeof chunkObj.type !== 'string') { + console.warn('Unknown chunk detected and skipped (missing type)', chunk); + return null; + } + + const { type } = chunkObj; + + if (type === 'markdown_text') { + if (typeof chunkObj.text === 'string') { + return chunkObj as unknown as MarkdownTextChunk; + } + console.warn('Invalid MarkdownTextChunk (missing text property)', chunk); + return null; + } + + if(type === 'plan_update') { + if (typeof chunkObj.title === 'string') { + return chunkObj as unknown as PlanUpdateChunk; + } + console.warn('Invalid PlanUpdateChunk (missing title property)', chunk); + return null; + } + + if (type === 'task_update') { + const taskChunk = chunkObj as Partial; + if ( + typeof taskChunk.id === 'string' && + typeof taskChunk.title === 'string' && + typeof taskChunk.status === 'string' && + ['pending', 'in_progress', 'complete', 'error'].includes(taskChunk.status) + ) { + return chunkObj as unknown as TaskUpdateChunk; + } + console.warn('Invalid TaskUpdateChunk (missing required properties)', chunk); + return null; + } + + console.warn(`Unknown chunk type detected and skipped: ${type}`, chunk); + return null; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 9992536de..2740a2290 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,6 +3,7 @@ export * from './block-kit/blocks'; export * from './block-kit/composition-objects'; export * from './block-kit/extensions'; export * from './calls'; +export * from './chunk'; export * from './dialog'; export * from './events'; export * from './message-attachments'; diff --git a/packages/web-api/package.json b/packages/web-api/package.json index 36ffc23bf..c41da4903 100644 --- a/packages/web-api/package.json +++ b/packages/web-api/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@slack/logger": "^4.0.0", - "@slack/types": "^2.18.0", + "@slack/types": "^2.19.0", "@types/node": ">=18.0.0", "@types/retry": "0.12.0", "axios": "^1.11.0", diff --git a/packages/web-api/src/types/request/chat.ts b/packages/web-api/src/types/request/chat.ts index 7734d7a7f..9c5c2292c 100644 --- a/packages/web-api/src/types/request/chat.ts +++ b/packages/web-api/src/types/request/chat.ts @@ -1,4 +1,5 @@ import type { + AnyChunk, Block, // TODO: these will be combined into one in a new types release EntityMetadata, KnownBlock, @@ -168,7 +169,13 @@ export interface Unfurls { unfurl_media?: boolean; } -export interface ChatAppendStreamArguments extends TokenOverridable, ChannelAndTS, MarkdownText {} +export interface ChatAppendStreamArguments extends TokenOverridable, ChannelAndTS, Partial { + /** + * @description An array of {@link https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming chunk objects} to append to the stream. + * Either `markdown_text` or `chunks` is required. + */ + chunks?: AnyChunk[]; +} // https://docs.slack.dev/reference/methods/chat.delete export interface ChatDeleteArguments extends ChannelAndTS, AsUser, TokenOverridable {} @@ -233,6 +240,11 @@ export type ChatScheduledMessagesListArguments = OptionalArgument< >; export interface ChatStartStreamArguments extends TokenOverridable, Channel, Partial, ThreadTS { + /** + * @description An array of {@link https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming chunk objects} to start the stream with. + * Either `markdown_text` or `chunks` is required. + */ + chunks?: AnyChunk[]; /** * @description The ID of the team that is associated with `recipient_user_id`. * This is required when starting a streaming conversation outside of a DM. @@ -249,6 +261,10 @@ export type ChatStopStreamArguments = TokenOverridable & ChannelAndTS & Partial & Partial & { + /** + * @description An array of {@link https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming chunk objects} to finalize the stream with. + */ + chunks?: AnyChunk[]; /** * Block formatted elements will be appended to the end of the message. */ diff --git a/packages/web-api/test/types/methods/chat.test-d.ts b/packages/web-api/test/types/methods/chat.test-d.ts index f1ec5f025..97ac7f59e 100644 --- a/packages/web-api/test/types/methods/chat.test-d.ts +++ b/packages/web-api/test/types/methods/chat.test-d.ts @@ -32,6 +32,30 @@ expectAssignable>([ markdown_text: 'hello', }, ]); +expectAssignable>([ + { + channel: 'C1234', + ts: '1234.56', + markdown_text: 'hello', + chunks: [ + { + type: 'markdown_text', + text: 'Hello world', + }, + { + type: 'plan_update', + title: 'Analyzing request', + }, + { + type: 'task_update', + id: 'task-1', + title: 'Processing request', + status: 'in_progress', + details: 'Working on it...', + }, + ], + }, +]); // chat.delete // -- sad path @@ -631,11 +655,51 @@ expectAssignable>([ markdown_text: 'hello', }, ]); +expectAssignable>([ + { + channel: 'C1234', + thread_ts: '1234.56', + chunks: [ + { + type: 'markdown_text', + text: 'Hello world', + }, + { + type: 'plan_update', + title: 'Analyzing request', + }, + { + type: 'task_update', + id: 'task-1', + title: 'Processing request', + status: 'in_progress', + details: 'Working on it...', + }, + ], + }, +]); expectAssignable>([ { channel: 'C1234', thread_ts: '1234.56', markdown_text: 'hello', + chunks: [ + { + type: 'markdown_text', + text: 'Hello world', + }, + { + type: 'plan_update', + title: 'Analyzing request', + }, + { + type: 'task_update', + id: 'task-1', + title: 'Processing request', + status: 'in_progress', + details: 'Working on it...', + }, + ], recipient_team_id: 'T1234', recipient_user_id: 'U1234', }, @@ -670,6 +734,30 @@ expectAssignable>([ blocks: [], }, ]); +expectAssignable>([ + { + channel: 'C1234', + ts: '1234.56', + chunks: [ + { + type: 'markdown_text', + text: 'Hello world', + }, + { + type: 'plan_update', + title: 'Analyzing request', + }, + { + type: 'task_update', + id: 'task-1', + title: 'Processing request', + status: 'in_progress', + details: 'Working on it...', + }, + ], + blocks: [], + }, +]); // chat.unfurl // -- sad path