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
27 changes: 27 additions & 0 deletions packages/types/src/block-kit/block-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,33 @@ export interface URLInput extends Actionable, Dispatchable, Focusable, Placehold
initial_value?: string;
}

/**
* @description A URL source element to reference in a task card block.
* @see {@link https://docs.slack.dev/reference/block-kit/block-elements/url-source-element}
*/

export interface URLSourceElement {
/**
* @description The type of element. In this case `type` is always `url`.
*/
type: 'url';

/**
* @description The URL type source.
*/
url: string;

/**
* @description Display text for the URL.
*/
text: string;

/**
* @description Icon URL to display with the source.
*/
icon_url?: string[];
}

/**
* @description Allows users to run a {@link https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-link-triggers/#workflow_buttons link trigger} with customizable inputs.
* @see {@link https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element Workflow button element reference}.
Expand Down
70 changes: 70 additions & 0 deletions packages/types/src/block-kit/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
Select,
Timepicker,
URLInput,
URLSourceElement,
WorkflowButton,
} from './block-elements';
import type {
Expand Down Expand Up @@ -64,6 +65,8 @@ export type KnownBlock =
| RichTextBlock
| SectionBlock
| TableBlock
| TaskCardBlock
| PlanUpdateBlock
| VideoBlock;

/**
Expand Down Expand Up @@ -402,6 +405,73 @@ interface TableBlockColumnSettings {
is_wrapped?: boolean;
}

/**
* @description A discrete action or tool call.
* @see https://docs.slack.dev/reference/block-kit/blocks/task-card-block/
*/
export interface TaskCardBlock extends Block {
/**
* @description The type of element. In this case `type` is always `task_card`.
*/
type: 'task_card';

/**
* @description ID for the task.
*/
task_id: string;

/**
* @description Title of the task in plain text.
*/
title: string;

/**
* @description Details of the task in the form of a single `rich_text` block.
*/
details?: RichTextBlock | Record<string, unknown>;

/**
* @description Output of the task in the form of a single "rich_text" entity.
*/
output?: RichTextBlock | Record<string, unknown>;

/**
* @description List of sources used to generate a response.
*/
sources?: (URLSourceElement | Record<string, unknown>)[]

/**
* @description The state of a task. Either `pending`, `in_progress`, `complete`, or `error`.
*/
status: 'pending' | 'in_progress' | 'complete' | 'error';
}

/**
* @description A collection of related tasks.
* @see https://docs.slack.dev/reference/block-kit/blocks/plan-block/
*/
export interface PlanUpdateBlock extends Block {
/**
* @description The type of block. In this case `type` is always `plan`.
*/
type: 'plan';

/**
* @description ID for the plan.
*/
plan_id: string;

/**
* @description Title of the plan in plain text.
*/
title: string;

/**
* @description An array of tasks associated with this plan.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @description An array of tasks associated with this plan.
* @description A sequence of task card blocks. Each task represents a single action within the plan.

*/
tasks?: (TaskCardBlock | Record<string, unknown>)[];
}

/**
* @description Displays an embedded video player. A video block is designed to embed videos in all app surfaces (e.g.
* link unfurls, messages, modals, App Home) — anywhere you can put blocks! To use the video block within your app, you
Expand Down
97 changes: 97 additions & 0 deletions packages/types/src/chunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { URLSourceElement } from "./block-kit/block-elements";
/**
* 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;
}

/**
* Used for updating the title of a plan.
* 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Used for displaying tool execution progress in a timeline-style UI.
* Used for displaying task 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?: URLSourceElement[];
}

/**
* 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);
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<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;
}
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';
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
23 changes: 16 additions & 7 deletions packages/web-api/src/chat-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ export class ChatStreamer {
if (args.token) {
this.token = args.token;
}
this.buffer += args.markdown_text;

if (args.chunks) {
return await this.flushBuffer(args);
}

if (args.markdown_text) {
this.buffer += args.markdown_text;
}
if (this.buffer.length >= this.options.buffer_size) {
return await this.flushBuffer(args);
}
Expand Down Expand Up @@ -144,12 +151,13 @@ export class ChatStreamer {
this.streamTs = response.ts;
this.state = 'in_progress';
}
const finalArgs = args?.chunks ? args : { ...args, markdown_text: this.buffer };

const response = await this.client.chat.stopStream({
token: this.token,
channel: this.streamArgs.channel,
ts: this.streamTs,
...args,
markdown_text: this.buffer,
...finalArgs,
});
this.state = 'completed';
return response;
Expand All @@ -158,12 +166,14 @@ export class ChatStreamer {
private async flushBuffer(
args: Omit<ChatStartStreamArguments | ChatAppendStreamArguments, 'channel' | 'ts'>,
): Promise<ChatStartStreamResponse | ChatAppendStreamResponse> {

const finalArgs = args.chunks ? args : { ...args, markdown_text: this.buffer };

if (!this.streamTs) {
const response = await this.client.chat.startStream({
...this.streamArgs,
token: this.token,
...args,
markdown_text: this.buffer,
...finalArgs,
});
this.buffer = '';
this.streamTs = response.ts;
Expand All @@ -174,8 +184,7 @@ export class ChatStreamer {
token: this.token,
channel: this.streamArgs.channel,
ts: this.streamTs,
...args,
markdown_text: this.buffer,
...finalArgs,
});
this.buffer = '';
return response;
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
Loading
Loading