diff --git a/packages/types/package.json b/packages/types/package.json index 59af1faee..e5210345a 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -27,6 +27,9 @@ "bugs": { "url": "https://github.com/slackapi/node-slack-sdk/issues" }, + "dependencies": { + "@slack/logger": "^4.0.0" + }, "scripts": { "prepare": "npm run build", "build": "npm run build:clean && tsc", diff --git a/packages/types/src/chunk.ts b/packages/types/src/chunk.ts new file mode 100644 index 000000000..892c31d21 --- /dev/null +++ b/packages/types/src/chunk.ts @@ -0,0 +1,109 @@ +import { ConsoleLogger, LogLevel } from '@slack/logger'; + +const logger = new ConsoleLogger(); +logger.setLevel(LogLevel.DEBUG); + +/** + * 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; + } + + if (!('type' in chunk) || typeof chunk.type !== 'string') { + logger.debug('Unknown chunk detected and skipped (missing type)', chunk); + return null; + } + + const type = chunk.type; + + if (type === 'markdown_text') { + if ('text' in chunk && typeof chunk.text === 'string') { + return chunk as MarkdownTextChunk; + } + logger.debug('Invalid MarkdownTextChunk (missing text property)', chunk); + return null; + } + + if (type === 'plan_update') { + if ('title' in chunk && typeof chunk.title === 'string') { + return chunk as PlanUpdateChunk; + } + logger.debug('Invalid PlanUpdateChunk (missing title property)', chunk); + return null; + } + + if (type === 'task_update') { + const taskChunk = chunk as Partial; + if ( + typeof taskChunk.id === 'string' && + typeof taskChunk.title === 'string' && + typeof taskChunk.status === 'string' && + ['pending', 'in_progress', 'complete', 'error'].includes(taskChunk.status) + ) { + return chunk as TaskUpdateChunk; + } + logger.debug('Invalid TaskUpdateChunk (missing required properties)', chunk); + return null; + } + + logger.debug(`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..7e6275354 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,29 @@ expectAssignable>([ markdown_text: 'hello', }, ]); +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...', + }, + ], + }, +]); // chat.delete // -- sad path @@ -631,6 +654,29 @@ 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', @@ -670,6 +716,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