Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions packages/types/src/chunk.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

if (!('type' in chunkObj) || typeof chunkObj.type !== 'string') {
console.warn('Unknown chunk detected and skipped (missing type)', chunk);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪵 issue: We should prefer the @slack/logger package for these outputs!

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') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if(type === 'plan_update') {
if (type === 'plan_update') {

👁️‍🗨️ issue: This is causing the linter to error and packages to not build as expected!

src/chunk.ts format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  × Formatter would have printed the following content:
  
    82 │ ··if·(type·===·'plan_update')·{
Checked 48 files in 26ms. No fixes applied.
       │     +                          

Found 1 error.
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🔗 https://github.com/slackapi/node-slack-sdk/actions/runs/21079717525/job/60630237979#step:9:13

if (typeof chunkObj.title === 'string') {
return chunkObj as unknown as PlanUpdateChunk;
}
console.warn('Invalid PlanUpdateChunk (missing title property)', chunk);
return null;
}
Comment on lines +82 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ suggestion: It might be preferable to let the API return an error for invalid chunks since these limitations might change in the future?


if (type === 'task_update') {
const taskChunk = chunkObj as Partial<TaskUpdateChunk>;
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👾 thought: Morphing to unknown then a type might have an alternative with type "narrowing"! IIRC @WilliamBergamin is most familiar with that approach and has shared these docs with me at a point:

📚 https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing

}
console.warn('Invalid TaskUpdateChunk (missing required properties)', chunk);
return null;
}

console.warn(`Unknown chunk type detected and skipped: ${type}`, chunk);
return null;
}
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 note: Right now I notice CI is failing for a missing export... Am curious if this is specific to our testing workflows or this PR, but will explore this adjacent!

export * from './dialog';
export * from './events';
export * from './message-attachments';
Expand Down
2 changes: 1 addition & 1 deletion packages/web-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 17 additions & 1 deletion packages/web-api/src/types/request/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
AnyChunk,
Block, // TODO: these will be combined into one in a new types release
EntityMetadata,
KnownBlock,
Expand Down Expand Up @@ -168,7 +169,13 @@ export interface Unfurls {
unfurl_media?: boolean;
}

export interface ChatAppendStreamArguments extends TokenOverridable, ChannelAndTS, MarkdownText {}
export interface ChatAppendStreamArguments extends TokenOverridable, ChannelAndTS, Partial<MarkdownText> {
/**
* @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 {}
Expand Down Expand Up @@ -233,6 +240,11 @@ export type ChatScheduledMessagesListArguments = OptionalArgument<
>;

export interface ChatStartStreamArguments extends TokenOverridable, Channel, Partial<MarkdownText>, 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.
Expand All @@ -249,6 +261,10 @@ export type ChatStopStreamArguments = TokenOverridable &
ChannelAndTS &
Partial<MarkdownText> &
Partial<Metadata> & {
/**
* @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.
*/
Expand Down
88 changes: 88 additions & 0 deletions packages/web-api/test/types/methods/chat.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ expectAssignable<Parameters<typeof web.chat.appendStream>>([
markdown_text: 'hello',
},
]);
expectAssignable<Parameters<typeof web.chat.appendStream>>([
{
channel: 'C1234',
ts: '1234.56',
markdown_text: 'hello',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
markdown_text: 'hello',

🪓 issue: Let's remove this to match expected API behavior!

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
Expand Down Expand Up @@ -631,11 +655,51 @@ expectAssignable<Parameters<typeof web.chat.startStream>>([
markdown_text: 'hello',
},
]);
expectAssignable<Parameters<typeof web.chat.startStream>>([
{
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<Parameters<typeof web.chat.startStream>>([
{
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...',
},
],
Comment on lines +686 to +702
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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...',
},
],

🪓 issue: Similar to the above, I'm not sure if we should error in typescript if both "markdown_text" and "chunks" are provided, but let's not showcase that here IMHO

recipient_team_id: 'T1234',
recipient_user_id: 'U1234',
},
Expand Down Expand Up @@ -670,6 +734,30 @@ expectAssignable<Parameters<typeof web.chat.stopStream>>([
blocks: [],
},
]);
expectAssignable<Parameters<typeof web.chat.stopStream>>([
{
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
Expand Down
Loading