Skip to content

Commit 6c26a7f

Browse files
fix(slack): enforce agent API compatibility
1 parent a922946 commit 6c26a7f

15 files changed

Lines changed: 180 additions & 47 deletions

File tree

apps/docs/content/docs/integrations/slack.mdx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,29 @@ Get a stable permalink URL to a specific Slack message.
803803
| `channel` | string | Channel ID containing the message |
804804
| `permalink` | string | The permalink URL to the message |
805805

806+
### Slack Set Suggested Prompts
807+
808+
Set the clickable suggested prompts shown in a Slack assistant thread (the prompt chips in an AI app).
809+
810+
#### Input
811+
812+
| Parameter | Type | Required | Description |
813+
| --------- | ---- | -------- | ----------- |
814+
| `authMethod` | string | No | Authentication method: oauth or bot_token |
815+
| `botToken` | string | No | Bot token for Custom Bot |
816+
| `channel` | string | Yes | Channel ID containing the assistant thread \(e.g., C1234567890 or D1234567890\) |
817+
| `threadTs` | string | Yes | Thread timestamp \(thread_ts\) of the assistant thread \(e.g., 1405894322.002768\) |
818+
| `prompts` | json | Yes | Array of prompts, each with a "title" \(shown on the chip\) and a "message" \(sent when clicked\). Max 4. |
819+
| `promptsTitle` | string | No | Optional heading for the prompt list, e.g. 'Suggested Prompts' |
820+
821+
#### Output
822+
823+
| Parameter | Type | Description |
824+
| --------- | ---- | ----------- |
825+
| `ok` | boolean | Whether the suggested prompts were set successfully |
826+
| `channel` | string | Channel ID the prompts were set on |
827+
| `threadTs` | string | Thread timestamp the prompts were set on |
828+
806829
### Slack Set Agent Suggested Prompts
807830

808831
Set suggested prompts in Slack Agent View, optionally scoped to a specific thread.
@@ -850,7 +873,7 @@ Create or update the state of a Slack agent session associated with a thread.
850873
| `ok` | boolean | Whether Slack updated the agent session |
851874
| `status` | string | Requested agent session status |
852875
| `agentStatus` | string | Agent status recorded by Slack |
853-
| `title` | string | Current agent session title |
876+
| `title` | string | Current agent session title, or null when the session has no title |
854877

855878
### Slack Rename Agent Session
856879

@@ -887,8 +910,8 @@ Start a streaming Slack message using Markdown or structured chunks.
887910
| `markdownText` | string | No | Initial Markdown content, mutually exclusive with chunks |
888911
| `chunks` | json | No | Initial structured Slack streaming chunks, mutually exclusive with Markdown |
889912
| `threadTs` | string | No | Parent thread timestamp when streaming a reply |
890-
| `recipientUserId` | string | No | Recipient user ID for a channel stream |
891-
| `recipientTeamId` | string | No | Recipient workspace ID for a channel stream |
913+
| `recipientUserId` | string | No | Recipient user ID, required with Recipient Team ID when channel is not a DM |
914+
| `recipientTeamId` | string | No | Recipient workspace ID, required with Recipient User ID when channel is not a DM |
892915
| `taskDisplayMode` | string | No | Task display mode: timeline or plan |
893916
| `iconEmoji` | string | No | Emoji used to customize the streaming agent identity |
894917
| `iconUrl` | string | No | Image URL used to customize the streaming agent identity |
@@ -914,8 +937,8 @@ Append Markdown or structured chunks to an active Slack stream.
914937
| `botToken` | string | No | Custom Slack bot token |
915938
| `channel` | string | Yes | Channel containing the streaming message |
916939
| `ts` | string | Yes | Timestamp returned by Start Stream |
917-
| `markdownText` | string | No | Markdown content to append, mutually exclusive with chunks |
918-
| `chunks` | json | No | Structured Slack streaming chunks, mutually exclusive with Markdown |
940+
| `markdownText` | string | No | Markdown content to append; provide this or chunks, but not both |
941+
| `chunks` | json | No | Structured Slack streaming chunks; provide this or Markdown Text, but not both |
919942

920943
#### Output
921944

@@ -952,10 +975,10 @@ Finalize an active Slack stream and return the resulting message.
952975
| `ts` | string | Finalized streaming message timestamp |
953976
| `message` | object | Final Slack message returned by chat.stopStream |
954977
|`text` | string | Final message text |
955-
|`bot_id` | string | Bot ID |
978+
|`bot_id` | string | Bot ID, or null when Slack omits it |
956979
|`ts` | string | Message timestamp |
957980
|`type` | string | Message type |
958-
|`subtype` | string | Message subtype |
981+
|`subtype` | string | Message subtype, or null when the message has no subtype |
959982

960983
### Slack List Channels
961984

apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,18 @@ function getAgentConfigurationError(
7070
if (prompts.some((prompt) => !prompt.title.trim() || !prompt.message.trim())) {
7171
return 'Every suggested prompt needs a title and message.'
7272
}
73+
if (prompts.length > 4) {
74+
return 'Slack Agent View supports at most four suggested prompts.'
75+
}
7376
return null
7477
}
7578

79+
function getAgentDescriptionError(description: string): string | null {
80+
return description.trim().length > 300
81+
? 'Slack Agent View descriptions must be 300 characters or fewer.'
82+
: null
83+
}
84+
7685
interface ConnectSlackBotModalProps {
7786
open: boolean
7887
onOpenChange: (open: boolean) => void
@@ -152,10 +161,12 @@ export function ConnectSlackBotModal({
152161
// window.location.origin) so Slack's servers can reach it.
153162
const requestUrl = useMemo(() => buildSlackCustomBotRequestUrl(credentialId), [credentialId])
154163

164+
const descriptionError = getAgentDescriptionError(appDescription)
155165
const agentConfigurationError = getAgentConfigurationError(agentActions, suggestedPrompts)
166+
const manifestConfigurationError = descriptionError ?? agentConfigurationError
156167

157168
const manifestJson = useMemo(() => {
158-
if (agentConfigurationError) return ''
169+
if (manifestConfigurationError) return ''
159170
const managedUserAuthorization = selected.has(SLACK_MANAGED_USER_AUTHORIZATION_CAPABILITY.id)
160171
? getSlackManagedUserAuthorizationManifestConfig(getBaseUrl())
161172
: undefined
@@ -169,7 +180,7 @@ export function ConnectSlackBotModal({
169180
})
170181
return JSON.stringify(manifest, null, 2)
171182
}, [
172-
agentConfigurationError,
183+
manifestConfigurationError,
173184
selected,
174185
appName,
175186
appDescription,
@@ -255,12 +266,16 @@ export function ConnectSlackBotModal({
255266
{/* Bot name is required so the credential name, the manifest app name, and
256267
uniqueness all use the user's choice — never the shared Slack team name
257268
fallback, which collides for a second bot in the same workspace. */}
258-
<Wizard.Step title='Configure your bot' canAdvance={appName.trim().length > 0}>
269+
<Wizard.Step
270+
title='Configure your bot'
271+
canAdvance={appName.trim().length > 0 && !descriptionError}
272+
>
259273
<StepConfigure
260274
appName={appName}
261275
onAppNameChange={setAppName}
262276
appDescription={appDescription}
263277
onAppDescriptionChange={setAppDescription}
278+
descriptionError={descriptionError}
264279
capabilityIds={capabilityIds}
265280
onCapabilityIdsChange={setCapabilityIds}
266281
/>
@@ -319,6 +334,7 @@ interface StepConfigureProps {
319334
onAppNameChange: (next: string) => void
320335
appDescription: string
321336
onAppDescriptionChange: (next: string) => void
337+
descriptionError: string | null
322338
capabilityIds: string[]
323339
onCapabilityIdsChange: (next: string[]) => void
324340
}
@@ -327,6 +343,7 @@ function StepConfigure({
327343
onAppNameChange,
328344
appDescription,
329345
onAppDescriptionChange,
346+
descriptionError,
330347
capabilityIds,
331348
onCapabilityIdsChange,
332349
}: StepConfigureProps) {
@@ -355,7 +372,11 @@ function StepConfigure({
355372
onChange={(e) => onAppDescriptionChange(e.target.value)}
356373
placeholder="Optional — shown on the bot's Slack profile"
357374
maxLength={140}
375+
error={Boolean(descriptionError)}
358376
/>
377+
{descriptionError && (
378+
<p className='text-[var(--text-error)] text-caption'>{descriptionError}</p>
379+
)}
359380
</div>
360381
<div className='flex flex-col gap-[9px]'>
361382
<Label className='text-[var(--text-muted)] text-small'>Additional permissions</Label>
@@ -517,7 +538,12 @@ function StepAgentView({
517538
</Button>
518539
</div>
519540
))}
520-
<Chip className='w-fit' leftIcon={Plus} onClick={addSuggestedPrompt}>
541+
<Chip
542+
className='w-fit'
543+
leftIcon={Plus}
544+
onClick={addSuggestedPrompt}
545+
disabled={suggestedPrompts.length >= 4}
546+
>
521547
Add suggested prompt
522548
</Chip>
523549
</div>

apps/sim/blocks/blocks/slack.test.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,14 @@ describe('Slack block release', () => {
4949

5050
it('replaces legacy assistant operations with custom-bot Agent Sessions operations', () => {
5151
expect(operationIds()).toEqual(expect.arrayContaining(AGENT_OPERATION_IDS))
52-
expect(operationIds()).not.toEqual(expect.arrayContaining(['set_status', 'set_title']))
52+
for (const id of ['set_status', 'set_title']) {
53+
expect(operationIds()).not.toContain(id)
54+
}
5355
expect(getSlackV2ToolAccess()).toEqual(expect.arrayContaining(AGENT_TOOL_IDS))
54-
expect(getSlackV2ToolAccess()).not.toEqual(
55-
expect.arrayContaining(['slack_set_status', 'slack_set_title', 'slack_set_suggested_prompts'])
56-
)
56+
expect(getSlackV2ToolAccess()).toContain('slack_set_suggested_prompts')
57+
for (const id of ['slack_set_status', 'slack_set_title']) {
58+
expect(getSlackV2ToolAccess()).not.toContain(id)
59+
}
5760
expect(Object.keys(getSlackV2OperationSentences())).toEqual(
5861
expect.arrayContaining(AGENT_OPERATION_IDS)
5962
)
@@ -70,22 +73,54 @@ describe('Slack block release', () => {
7073
})
7174
})
7275

73-
it('preserves persisted suggested-prompt inputs while requiring the custom-bot tool', () => {
76+
it('keeps persisted OAuth suggested prompts on the compatibility tool', () => {
77+
const selectTool = SlackV2Block.tools.config?.tool
78+
if (!selectTool) throw new Error('Slack v2 tool selector is required')
79+
80+
expect(
81+
selectTool({ operation: 'set_suggested_prompts', oauthCredential: 'oauth-credential' })
82+
).toBe('slack_set_suggested_prompts')
7483
expect(
7584
mapSlackV2Params({
7685
operation: 'set_suggested_prompts',
77-
oauthCredential: 'custom-bot-credential',
86+
oauthCredential: 'oauth-credential',
7887
channel: 'C123',
7988
getThreadTimestamp: '1700000000.000001',
8089
suggestedPrompts: '[{"title":"Summarize","message":"Summarize this thread"}]',
8190
promptsTitle: 'Try asking',
8291
})
8392
).toMatchObject({
84-
credential: 'custom-bot-credential',
93+
credential: 'oauth-credential',
8594
channel: 'C123',
8695
threadTs: '1700000000.000001',
8796
prompts: '[{"title":"Summarize","message":"Summarize this thread"}]',
8897
promptsTitle: 'Try asking',
8998
})
9099
})
100+
101+
it('uses the service-account tool and active content mode for new agent operations', () => {
102+
const selectTool = SlackV2Block.tools.config?.tool
103+
if (!selectTool) throw new Error('Slack v2 tool selector is required')
104+
105+
expect(
106+
selectTool({ operation: 'set_suggested_prompts', agentCredentialId: 'custom-bot' })
107+
).toBe('slack_set_suggested_prompts_v2')
108+
109+
const mapped = mapSlackV2Params({
110+
operation: 'append_stream',
111+
agentCredentialId: 'custom-bot',
112+
agentChannelId: 'C123',
113+
streamTs: '1700000000.000001',
114+
streamContentMode: 'markdown',
115+
streamMarkdownText: 'Current content',
116+
streamChunks: [{ type: 'markdown_text', text: 'Stale content' }],
117+
})
118+
expect(mapped).toMatchObject({
119+
credential: 'custom-bot',
120+
channel: 'C123',
121+
ts: '1700000000.000001',
122+
markdownText: 'Current content',
123+
})
124+
expect(mapped.chunks).toBeUndefined()
125+
})
91126
})

apps/sim/blocks/blocks/slack.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3447,6 +3447,7 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
34473447
'slack_get_thread_replies',
34483448
'slack_get_channel_history',
34493449
'slack_get_permalink',
3450+
'slack_set_suggested_prompts',
34503451
'slack_set_suggested_prompts_v2',
34513452
'slack_set_agent_session_status_v2',
34523453
'slack_rename_agent_session_v2',
@@ -3488,7 +3489,9 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
34883489
tool: (params) => {
34893490
switch (params.operation) {
34903491
case 'set_suggested_prompts':
3491-
return 'slack_set_suggested_prompts_v2'
3492+
return params.agentCredentialId
3493+
? 'slack_set_suggested_prompts_v2'
3494+
: 'slack_set_suggested_prompts'
34923495
case 'set_agent_session_status':
34933496
return 'slack_set_agent_session_status_v2'
34943497
case 'rename_agent_session':
@@ -3511,26 +3514,21 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
35113514
if (!mapParams) throw new Error('Slack parameter mapper is required')
35123515
const baseParams = mapParams(params)
35133516
if (!SLACK_V2_AGENT_OPERATIONS.includes(params.operation as never)) return baseParams
3514-
3515-
const persistedSuggestedPromptParams =
3516-
params.operation === 'set_suggested_prompts'
3517-
? {
3518-
credential: baseParams.credential,
3519-
channel: baseParams.channel,
3520-
threadTs: baseParams.threadTs,
3521-
}
3522-
: undefined
3517+
if (params.operation === 'set_suggested_prompts' && !params.agentCredentialId) {
3518+
return baseParams
3519+
}
35233520

35243521
return {
35253522
...baseParams,
3526-
credential: params.agentCredentialId ?? persistedSuggestedPromptParams?.credential,
3527-
channel: params.agentChannelId ?? persistedSuggestedPromptParams?.channel,
3528-
threadTs: params.agentThreadTs ?? persistedSuggestedPromptParams?.threadTs,
3523+
credential: params.agentCredentialId,
3524+
channel: params.agentChannelId,
3525+
threadTs: params.agentThreadTs,
35293526
status: params.agentSessionStatus,
35303527
title: params.agentSessionTitle,
35313528
initiatorUserId: params.agentInitiatorUserId,
3532-
markdownText: params.streamMarkdownText,
3533-
chunks: params.streamChunks,
3529+
markdownText:
3530+
params.streamContentMode === 'markdown' ? params.streamMarkdownText : undefined,
3531+
chunks: params.streamContentMode === 'chunks' ? params.streamChunks : undefined,
35343532
ts: params.streamTs,
35353533
recipientUserId: params.streamRecipientUserId,
35363534
recipientTeamId: params.streamRecipientTeamId,

apps/sim/lib/oauth/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
314314
'chat:write': 'Send messages',
315315
'chat:write.public': 'Post to public channels',
316316
'chat:write.customize': 'Customize message username and icon',
317-
'assistant:write': 'Set suggested prompts for Agent View',
317+
'assistant:write': 'Manage assistant status, titles, and suggested prompts',
318318
'im:write': 'Send direct messages',
319319
'im:history': 'Read direct message history',
320320
'im:read': 'View direct message channels',

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/generated/tool-outputs.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4479,6 +4479,8 @@ describe('Copilot OAuth Credential Enforcement', () => {
44794479
credential: 'custom-slack-bot',
44804480
channel: 'C1',
44814481
markdownText: 'hello',
4482+
recipientUserId: 'U1',
4483+
recipientTeamId: 'T1',
44824484
})
44834485

44844486
expect(result).toMatchObject({

apps/sim/tools/slack/agent_sessions.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ describe('Slack Agent Sessions tools', () => {
2828
for (const tool of AGENT_TOOLS) {
2929
expect(tool.oauth).toMatchObject({
3030
provider: 'slack',
31-
requiredScopes: ['chat:write'],
3231
credentialKind: 'service-account',
3332
})
33+
expect(tool.oauth?.requiredScopes).toContain('chat:write')
3434
expect(requestOf(tool).url({})).toMatch(/^https:\/\/slack\.com\/api\//)
3535
}
36+
for (const tool of [slackSetAgentSessionStatusV2Tool, slackStartStreamV2Tool]) {
37+
expect(tool.oauth?.requiredScopes).toContain('chat:write.customize')
38+
}
3639
})
3740

3841
it('maps the documented session status request and response', async () => {
@@ -147,6 +150,24 @@ describe('Slack streaming tools', () => {
147150
})
148151
})
149152

153+
it('requires recipient IDs for channel streams but not direct messages', () => {
154+
const request = requestOf(slackStartStreamV2Tool)
155+
const base = {
156+
accessToken: 'xoxb-token',
157+
authMethod: 'bot_token',
158+
botToken: '',
159+
markdownText: 'First',
160+
}
161+
162+
expect(() => request.body?.({ ...base, channel: 'C1' })).toThrow(
163+
'Recipient User ID and Recipient Team ID are required for channel streams'
164+
)
165+
expect(request.body?.({ ...base, channel: 'D1' })).toEqual({
166+
channel: 'D1',
167+
markdown_text: 'First',
168+
})
169+
})
170+
150171
it('rejects mixed streaming content modes and empty appends', () => {
151172
const append = requestOf(slackAppendStreamV2Tool)
152173
const base = {

apps/sim/tools/slack/append_stream_v2.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,14 @@ export const slackAppendStreamV2Tool: ToolConfig<SlackAppendStreamV2Params, Slac
6161
type: 'string',
6262
required: false,
6363
visibility: 'user-or-llm',
64-
description: 'Markdown content to append, mutually exclusive with chunks',
64+
description: 'Markdown content to append; provide this or chunks, but not both',
6565
},
6666
chunks: {
6767
type: 'json',
6868
required: false,
6969
visibility: 'user-or-llm',
70-
description: 'Structured Slack streaming chunks, mutually exclusive with Markdown',
70+
description:
71+
'Structured Slack streaming chunks; provide this or Markdown Text, but not both',
7172
},
7273
},
7374
request: {

0 commit comments

Comments
 (0)