From e8d69c6ecb05d3e26956dab97493600869e774fb Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 23 Jul 2026 01:03:46 -0700 Subject: [PATCH] feat(slack): GA slack_v2 with agent_view custom-bot manifests --- .../connect-slack-bot-modal.tsx | 2 +- apps/sim/blocks/blocks/slack.ts | 26 +++++++------ apps/sim/blocks/registry-maps.ts | 3 +- apps/sim/lib/webhooks/providers/slack.ts | 12 ++++++ apps/sim/triggers/slack/capabilities.test.ts | 26 ++++++++++--- apps/sim/triggers/slack/capabilities.ts | 39 ++++++++++++------- apps/sim/triggers/slack/shared.ts | 11 ++++++ 7 files changed, 86 insertions(+), 33 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx index 7526064411e..9660f2cec21 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx @@ -310,7 +310,7 @@ function StepConfigure({ {allSelected && (

Full access — the bot can read and send messages, react, upload files, and chat as an AI - assistant. + agent.

)} diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 1792db96c52..e413d0a12c0 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -21,9 +21,10 @@ export const SlackBlock: BlockConfig = { bgColor: '#611f69', icon: SlackIcon, triggerAllowed: true, - // Superseded by slack_v2, but stays discoverable until v2 GAs — hiding both - // would leave no Slack block in the toolbar while v2 is preview-gated. At v2 - // GA this becomes `hideFromToolbar: true` (superseded-version paradigm). + // Superseded by slack_v2 (GA): hidden from discovery like other legacy _vN + // blocks; existing workflows keep executing it. + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'slack_v2' }, subBlocks: [ { id: 'operation', @@ -2465,9 +2466,10 @@ Return ONLY the integer Unix timestamp - no explanations, no quotes, no extra te team_id: { type: 'string', description: 'Slack workspace/team ID' }, event_id: { type: 'string', description: 'Unique event identifier for the trigger' }, }, - // Trigger capabilities moved to slack_v2 so the trigger surfaces once. - // Legacy webhook trigger stays available while slack_v2 (which hosts the - // redesigned slack_oauth trigger) is preview-gated; drops at v2 GA. + // The legacy webhook trigger must STAY enabled: webhook-execution gates on + // blockConfig.triggers.enabled at runtime, so disabling it would break every + // deployed v1 Slack trigger workflow. New discovery surfaces only slack_v2 + // (this block is hideFromToolbar). triggers: { enabled: true, available: ['slack_webhook'], @@ -2627,6 +2629,11 @@ export const SlackBlockMeta = { ], } as const satisfies BlockMeta +export const SlackV2BlockMeta = { + tags: ['messaging', 'webhooks', 'automation'], + url: 'https://slack.com', +} as const satisfies BlockMeta + const SLACK_WEBHOOK_TRIGGER_SUBBLOCK_IDS = new Set( getTrigger('slack_webhook').subBlocks.map((sb) => sb.id) ) @@ -2677,11 +2684,8 @@ export const SlackV2Block: BlockConfig = { ...SlackBlock, type: 'slack_v2', hideFromToolbar: false, - // Preview-gated: hidden from every discovery surface until revealed via the - // block-visibility AppConfig (hosted) or PREVIEW_BLOCKS=slack_v2 (dev / - // self-host). At GA: drop this flag, add SlackV2BlockMeta + docs, and set - // hideFromToolbar on v1. - preview: true, + // The spread inherits v1's sunset marker — clear it: this IS the GA successor. + sunset: undefined, subBlocks: [ ...SlackBlock.subBlocks.flatMap((sb) => { // Drop the legacy paste-secret trigger config (v1 hosts slack_webhook) diff --git a/apps/sim/blocks/registry-maps.ts b/apps/sim/blocks/registry-maps.ts index d3bc8cea720..1eed1a6f194 100644 --- a/apps/sim/blocks/registry-maps.ts +++ b/apps/sim/blocks/registry-maps.ts @@ -278,7 +278,7 @@ import { ShopifyBlock, ShopifyBlockMeta } from '@/blocks/blocks/shopify' import { SimWorkspaceEventBlock } from '@/blocks/blocks/sim_workspace_event' import { SimilarwebBlock, SimilarwebBlockMeta } from '@/blocks/blocks/similarweb' import { SixtyfourBlock, SixtyfourBlockMeta } from '@/blocks/blocks/sixtyfour' -import { SlackBlock, SlackBlockMeta, SlackV2Block } from '@/blocks/blocks/slack' +import { SlackBlock, SlackBlockMeta, SlackV2Block, SlackV2BlockMeta } from '@/blocks/blocks/slack' import { SmtpBlock, SmtpBlockMeta } from '@/blocks/blocks/smtp' import { SportmonksBlock, SportmonksBlockMeta } from '@/blocks/blocks/sportmonks' import { SpotifyBlock, SpotifyBlockMeta } from '@/blocks/blocks/spotify' @@ -878,6 +878,7 @@ export const BLOCK_META_REGISTRY: Record = { similarweb: SimilarwebBlockMeta, sixtyfour: SixtyfourBlockMeta, slack: SlackBlockMeta, + slack_v2: SlackV2BlockMeta, smtp: SmtpBlockMeta, sportmonks: SportmonksBlockMeta, spotify: SpotifyBlockMeta, diff --git a/apps/sim/lib/webhooks/providers/slack.ts b/apps/sim/lib/webhooks/providers/slack.ts index fcd88767072..a5b02e62791 100644 --- a/apps/sim/lib/webhooks/providers/slack.ts +++ b/apps/sim/lib/webhooks/providers/slack.ts @@ -117,6 +117,13 @@ interface SlackTriggerEvent { * submissions). Null for non-block_actions payloads. */ state: Record | null + /** + * What the user is currently viewing, for `app_context_changed` (agent + * messaging experience): `{ entities: [{ type, value, team_id }] }`, ordered + * by relevance; an empty object when Slack has no entities. Null for every + * other event type. + */ + context: Record | null hasFiles: boolean files: SlackDownloadedFile[] } @@ -151,6 +158,7 @@ function createSlackEvent(): SlackTriggerEvent { view: null, message: null, state: null, + context: null, hasFiles: false, files: [], } @@ -605,6 +613,7 @@ export function resolveSlackEventKey(body: Record): string | nu case 'pin_removed': case 'team_join': case 'app_home_opened': + case 'app_context_changed': case 'assistant_thread_started': case 'assistant_thread_context_changed': return type @@ -977,6 +986,9 @@ export const slackHandler: WebhookProviderHandler = { event.message_ts = messageTs event.hasFiles = hasFiles event.files = files + if (eventType === 'app_context_changed') { + event.context = (rawEvent?.context as Record | undefined) ?? {} + } return { input: { event } } }, diff --git a/apps/sim/triggers/slack/capabilities.test.ts b/apps/sim/triggers/slack/capabilities.test.ts index be56ce1cd07..9d068d407de 100644 --- a/apps/sim/triggers/slack/capabilities.test.ts +++ b/apps/sim/triggers/slack/capabilities.test.ts @@ -31,7 +31,7 @@ describe('buildSlackManifest - interactivity', () => { }) describe('buildSlackManifest - description', () => { - it('emits display_information.description and reuses it as the assistant description', () => { + it('emits display_information.description and reuses it as the agent description', () => { const manifest = buildSlackManifest(new Set(['action_assistant']), { ...opts, description: 'Answers support questions.', @@ -41,15 +41,29 @@ describe('buildSlackManifest - description', () => { description: 'Answers support questions.', }) const features = manifest.features as Record> - expect(features.assistant_view.assistant_description).toBe('Answers support questions.') + expect(features.agent_view.agent_description).toBe('Answers support questions.') }) - it('omits the description key and falls back for the assistant when absent', () => { + it('omits the description key and falls back for the agent when absent', () => { const manifest = buildSlackManifest(new Set(['action_assistant']), opts) expect(manifest.display_information).toEqual({ name: 'Test Bot' }) const features = manifest.features as Record> - expect(features.assistant_view.assistant_description).toBe( - 'Test Bot — an AI assistant powered by Sim.' - ) + expect(features.agent_view.agent_description).toBe('Test Bot — an AI agent powered by Sim.') + }) + + it('caps the agent description at the 300-char manifest limit', () => { + const manifest = buildSlackManifest(new Set(['action_assistant']), { + ...opts, + description: 'x'.repeat(400), + }) + const features = manifest.features as Record> + expect((features.agent_view.agent_description as string).length).toBe(300) + }) + + it('subscribes the agent-experience events, not the deprecated assistant_thread_* set', () => { + const manifest = buildSlackManifest(new Set(['action_assistant']), opts) + const settings = settingsOf(manifest) + const events = (settings.event_subscriptions as Record).bot_events as string[] + expect(events).toEqual(['app_context_changed', 'app_home_opened', 'message.im']) }) }) diff --git a/apps/sim/triggers/slack/capabilities.ts b/apps/sim/triggers/slack/capabilities.ts index 26bae846059..50f46961d64 100644 --- a/apps/sim/triggers/slack/capabilities.ts +++ b/apps/sim/triggers/slack/capabilities.ts @@ -20,10 +20,12 @@ export interface SlackCapability { scopes: readonly string[] events: readonly string[] /** - * Marks the AI Assistant capability. When enabled the manifest additionally - * declares the app as an Agents & AI app (`features.assistant_view`) and - * enables the App Home messages tab — required for assistant threads, the - * "thinking" status (`assistant.threads.setStatus`), and DM-style chat to work. + * Marks the AI agent capability. When enabled the manifest additionally + * declares the app as an Agents & AI app (`features.agent_view` — the current + * agent messaging experience; new Slack apps cannot use the deprecated + * `assistant_view`) and enables the App Home messages tab — required for the + * agent surface, the "thinking" status (`assistant.threads.setStatus`), and + * DM-style chat to work. */ assistant?: boolean /** @@ -166,13 +168,18 @@ export const SLACK_CAPABILITIES: readonly SlackCapability[] = [ }, { id: 'action_assistant', - label: 'AI assistant', + label: 'AI agent', description: - 'Register the bot as an AI assistant: users open an assistant thread, the bot shows a "thinking" status, and can set the thread title and suggested prompts (assistant.threads.*).', + 'Register the bot as an AI agent: users chat with it in the agent surface, the bot shows a "thinking" status, and can set the thread title and suggested prompts (assistant.threads.*).', defaultChecked: true, group: 'action', scopes: ['assistant:write', 'im:history'], - events: ['assistant_thread_started', 'assistant_thread_context_changed', 'message.im'], + // The agent messaging experience's event set: app_home_opened (user opened + // the agent DM), app_context_changed (what the user is viewing; requires + // features.agent_view), message.im (incoming messages). The legacy + // assistant_thread_* events belong to the deprecated assistant_view + // experience and are not subscribable by new agent_view apps. + events: ['app_home_opened', 'app_context_changed', 'message.im'], assistant: true, }, { @@ -211,7 +218,7 @@ const WEBHOOK_URL_PLACEHOLDER = '' export interface BuildManifestOptions { appName: string webhookUrl: string | null - /** Shown on the bot's Slack profile and as the assistant description. */ + /** Shown on the bot's Slack profile and as the agent description. */ description?: string } @@ -241,12 +248,16 @@ export function buildSlackManifest( bot_user: { display_name: displayName, always_online: true }, } if (isAssistant) { - // Declares the app as an Agents & AI app; without this Slack won't surface - // the assistant thread UI or fire assistant_thread_* events. The messages - // tab must be enabled so users can chat the assistant. - features.assistant_view = { - assistant_description: - trimmedDescription || `${displayName} — an AI assistant powered by Sim.`, + // Declares the app as an Agents & AI app via the current agent messaging + // experience. New Slack apps can ONLY use agent_view (assistant_view is + // deprecated and rejected for new apps; switching an existing app to + // agent_view is irreversible). Without this Slack won't surface the agent + // UI or fire app_context_changed. The messages tab must be enabled so + // users can chat with the agent. agent_description caps at 300 chars. + features.agent_view = { + agent_description: ( + trimmedDescription || `${displayName} — an AI agent powered by Sim.` + ).slice(0, 300), } features.app_home = { home_tab_enabled: false, diff --git a/apps/sim/triggers/slack/shared.ts b/apps/sim/triggers/slack/shared.ts index 4372f7ba68b..144cea94a09 100644 --- a/apps/sim/triggers/slack/shared.ts +++ b/apps/sim/triggers/slack/shared.ts @@ -127,6 +127,11 @@ export const SLACK_TRIGGER_OUTPUTS: Record = { description: 'Timestamp of the message the interaction originated from. Present for block_actions', }, + context: { + type: 'json', + description: + 'What the user is currently viewing: an object with an entities array (each entry: type, value, team_id), e.g. context.entities[0].value. Present for app_context_changed; null otherwise', + }, view: { type: 'json', description: @@ -245,6 +250,12 @@ export const SLACK_EVENT_CATALOG: readonly SlackEventCatalogEntry[] = [ { id: 'pin_removed', label: 'Pin removed', simSubscribed: false, filters: ['channels'] }, { id: 'team_join', label: 'Member joined workspace', simSubscribed: false, filters: [] }, { id: 'app_home_opened', label: 'App home opened', simSubscribed: false, filters: [] }, + { + id: 'app_context_changed', + label: 'App context changed', + simSubscribed: false, + filters: [], + }, { id: 'assistant_thread_started', label: 'Assistant opened', simSubscribed: true, filters: [] }, { id: 'assistant_thread_context_changed',