Skip to content

feat: add support for sayStream listener argument#2841

Open
WilliamBergamin wants to merge 12 commits intomainfrom
say-stream
Open

feat: add support for sayStream listener argument#2841
WilliamBergamin wants to merge 12 commits intomainfrom
say-stream

Conversation

@WilliamBergamin
Copy link
Copy Markdown
Contributor

Summary

This PR aims to introduce a new listener argument utility sayStream, it enables developers to easily use a WebClient.chatStream object initialized with logical default values.

sayStream is WebClient.chatStream initialized with

  • channel_id: from the event payload
  • thread_ts: thread_ts from the event payload or falls back to the ts value if it is available
  • recipient_team_id: the team_id from the event received or the enterprise_id if the app is installed on the org
  • recipient_user_id: the user_id from the event received

sayStream is available on app.event and the assistant listeners, if Bolt fails to extract channel_id or thread_ts then sayStream will be undefined

Testing

  1. clone this branch
  2. build the project with npm pack .
  3. Import the package in a Bolt project
  4. Play around with sayStream
Sample app.ts
import { App, LogLevel } from '@slack/bolt';
import 'dotenv/config';

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

const app = new App({
token: process.env.SLACK_BOT_TOKEN,
socketMode: true,
appToken: process.env.SLACK_APP_TOKEN,
logLevel: LogLevel.DEBUG,
});

app.event('app_mention', async ({ sayStream }) => {
const stream = sayStream({ buffer_size: 100 });

await stream.append({ markdown_text: '*Someone rang the bat signal!* :bat:\n\n' });
await sleep(5000);
await stream.append({ markdown_text: "Oh wait, it's just an @mention. Even better!\n\n" });
await stream.append({
  markdown_text: "> Fun fact: I was mass-produced in a mass-production factory, but I like to think I'm *one of a kind*.\n\n",
});
await sleep(1000);
await stream.append({ markdown_text: "Anyway, here's my *totally professional* take:\n\n" });
await stream.append({
  markdown_text: "1. `sayStream` is basically magic — words appear like I'm *actually typing* :sparkles:\n",
});
await sleep(1000);
await stream.append({
  markdown_text: "2. Streaming means you don't have to stare at a blank screen wondering if I ghosted you\n",
});
await sleep(1000);
await stream.append({
  markdown_text: '3. Bolt for JavaScript makes building Slack apps easier than microwaving leftovers\n\n',
});
await sleep(500);
await stream.append({ markdown_text: '_*mic drop*_ :microphone:' });
await stream.stop();
});

app.message('', async ({ sayStream }) => {
const stream = sayStream({ buffer_size: 100 });

await stream.append({ markdown_text: '*Psst...* you just DMed a bot. Bold move. I respect that. :sunglasses:\n\n' });
await sleep(5000);
await stream.append({ markdown_text: 'Let me consult my *vast knowledge database*...\n\n' });
await stream.append({ markdown_text: '```\n[ scanning... ]\n[ found: 1 brain cell ]\n[ deploying it now ]\n```\n\n' });
await sleep(1000);
await stream.append({ markdown_text: 'Okay here\'s the deal:\n\n' });
await sleep(1000);
await stream.append({
  markdown_text: ':rocket: *Streaming responses* means you get to watch me think in real time — terrifying, I know\n',
});
await sleep(1000);
await stream.append({
  markdown_text: ':hammer_and_wrench: *Bolt for JavaScript* is the secret sauce behind my dazzling personality\n',
});
await sleep(1000);
await stream.append({
  markdown_text: ':zap: *Socket Mode* keeps our conversation nice and private — no nosy webhooks here\n\n',
});
await sleep(500);
await stream.append({ markdown_text: "That's all I've got. Don't forget to tip your bot! :robot_face:" });
await stream.stop();
});

(async () => {
try {
  await app.start(process.env.PORT || 3000);
  app.logger.info('Bolt app is running!');
} catch (error) {
  app.logger.error('Unable to start App', error);
}
})();
manifest.json
{
    "_metadata": {
        "major_version": 1,
        "minor_version": 1
    },
    "display_information": {
        "name": "SayStreamTest"
    },
    "features": {
        "app_home": {
            "home_tab_enabled": false,
            "messages_tab_enabled": true,
            "messages_tab_read_only_enabled": false
        },
        "bot_user": {
            "display_name": "SayStreamTest",
            "always_online": false
        }
    },
    "oauth_config": {
        "scopes": {
            "bot": [
                "app_mentions:read",
                "chat:write",
                "im:read",
                "im:write",
                "channels:history",
                "im:history"
            ]
        }
    },
    "settings": {
        "event_subscriptions": {
            "bot_events": [
                "app_mention",
                "message.im"
            ]
        },
        "interactivity": {
            "is_enabled": true
        },
        "org_deploy_enabled": true,
        "socket_mode_enabled": true,
        "token_rotation_enabled": false
    }
}

Requirements (place an x in each [ ])

@WilliamBergamin WilliamBergamin added this to the 4.7.0 milestone Mar 26, 2026
@WilliamBergamin WilliamBergamin self-assigned this Mar 26, 2026
@WilliamBergamin WilliamBergamin requested a review from a team as a code owner March 26, 2026 21:10
@WilliamBergamin WilliamBergamin added enhancement M-T: A feature request for new functionality semver:minor javascript Pull requests that update Javascript code labels Mar 26, 2026
Comment on lines +4 to +10
export interface SayStreamArguments {
buffer_size?: number;
channel?: string;
thread_ts?: string;
recipient_team_id?: string;
recipient_user_id?: string;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I made Bolt define its own type but maybe it better is we reused the one from WebClient.chatStream, what do y'all think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@WilliamBergamin Is it possible to define this as something like:

export interface SayStreamArguments = ArgumentType<WebClient['chatStream']>;

Since I agree it's clear to export this as SayStreamArguments but it'd be nice to find matching @jsdoc perhaps too?

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.58%. Comparing base (0dfaba0) to head (e5ee923).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2841      +/-   ##
==========================================
+ Coverage   93.50%   93.58%   +0.07%     
==========================================
  Files          42       43       +1     
  Lines        7747     7841      +94     
  Branches      678      689      +11     
==========================================
+ Hits         7244     7338      +94     
  Misses        498      498              
  Partials        5        5              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zimeg zimeg changed the title feat: add support for say_stream utility feat: add support for sayStream listener argument Mar 27, 2026
Copy link
Copy Markdown
Member

@zimeg zimeg left a comment

Choose a reason for hiding this comment

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

@WilliamBergamin Amazing efforts more toward surfacing the latest 🤖 ✨

I'm approving this now with a few thoughts toward exported types and tests but this is good stuff 🚢

Comment on lines +4 to +10
export interface SayStreamArguments {
buffer_size?: number;
channel?: string;
thread_ts?: string;
recipient_team_id?: string;
recipient_user_id?: string;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@WilliamBergamin Is it possible to define this as something like:

export interface SayStreamArguments = ArgumentType<WebClient['chatStream']>;

Since I agree it's clear to export this as SayStreamArguments but it'd be nice to find matching @jsdoc perhaps too?


/**
* Creates utility `sayStream()` to stream responses in the assistant thread.
* https://api.slack.com/methods/chat.stream
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* https://api.slack.com/methods/chat.stream
* https://docs.slack.dev/tools/bolt-js/concepts/message-sending#streaming-messages

📚 suggestion: I'm uncertain of the absolute link we use but this might be most current?

Comment on lines +219 to 220
assert.exists(assistantArgs.sayStream);
assert.exists(assistantArgs.setStatus);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🧮 quibble: Here and in a few other places the alphabetics are off. No blocker but might be a quick change?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🧪 suggestion: It'd be nice to include another listener type here for perhaps expected asserts? I'm not certain what correct behavior is at a glance, but I find the action listeners don't include this argument:

app.action('button_click', async ({ ack, sayStream }) => {
  await ack();
  const stream = sayStream({ buffer_size: 100 });
  // ...
});

Comment on lines +173 to +181
/**
* Extracts ts from the event payload.
*/
export function extractEventTs<T extends string>(event: KnownEventFromType<T>): string | undefined {
if (hasStringProperty(event, 'ts')) {
return event.ts;
}
return undefined;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔍 praise: I like the simple implementation to begin! Let's continue to enhance these extractions as edge cases appear I think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement M-T: A feature request for new functionality javascript Pull requests that update Javascript code semver:minor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants