From bd88e67df03f329f6b0ea0e6f4a0d9c4d8819f0f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 22 Jul 2026 19:58:01 -0700 Subject: [PATCH 1/2] feat(slack): slack-v2 copilot entitlement and agent_view custom-bot manifests --- .../connect-slack-bot-modal.tsx | 2 +- apps/sim/lib/copilot/entitlements.ts | 22 +++++++++++ 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 ++++++ 6 files changed, 91 insertions(+), 21 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/lib/copilot/entitlements.ts b/apps/sim/lib/copilot/entitlements.ts index 8ef560cacea..df704408b88 100644 --- a/apps/sim/lib/copilot/entitlements.ts +++ b/apps/sim/lib/copilot/entitlements.ts @@ -1,7 +1,10 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { LRUCache } from 'lru-cache' +import { getBlockVisibilityForCopilot } from '@/lib/copilot/block-visibility' import { isCustomBlocksEligible } from '@/lib/workflows/custom-blocks/operations' +import { SlackV2Block } from '@/blocks/blocks/slack' +import { isHiddenUnder } from '@/blocks/visibility/context' const logger = createLogger('CopilotEntitlements') @@ -10,6 +13,24 @@ const logger = createLogger('CopilotEntitlements') * its `core.Entitlement*` constants to gate agent surfaces. */ export const CUSTOM_BLOCKS_ENTITLEMENT = 'custom-blocks' +export const SLACK_V2_ENTITLEMENT = 'slack-v2' + +/** + * The slack_v2 surface (the preview block's tips, the workspace prompt's Slack + * tip listing, and the slack_custom_bot credential tag) follows the viewer's + * block-visibility projection — the same predicate that hides the slack_v2 + * schema in the copilot VFS — so tip and schema visibility cannot diverge. + * Argument-order flip is deliberate: evaluators are (workspaceId, userId?), + * getBlockVisibilityForCopilot is (userId, workspaceId). No userId → fail + * closed (visibility is per-viewer; an anonymous evaluation must not reveal a + * preview block). Once slack_v2 GAs (preview flag dropped), this evaluator + * returns true everywhere without changes. + */ +async function isSlackV2Visible(workspaceId: string, userId?: string): Promise { + if (!userId) return false + const vis = await getBlockVisibilityForCopilot(userId, workspaceId) + return !isHiddenUnder(vis, SlackV2Block) +} /** * Workspace entitlements — plan/flag-gated org capabilities sent to the @@ -33,6 +54,7 @@ const ENTITLEMENT_EVALUATORS: Record< (workspaceId: string, userId?: string) => Promise > = { [CUSTOM_BLOCKS_ENTITLEMENT]: isCustomBlocksEligible, + [SLACK_V2_ENTITLEMENT]: isSlackV2Visible, } const entitlementsCache = new LRUCache>({ 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..a9a3daa7f69 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, as an entities array (each entry: type, value, team_id). 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', From b6aab2c7778d0e66245b43818771f02ae9275d47 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 22 Jul 2026 20:12:45 -0700 Subject: [PATCH 2/2] fix(slack): describe app_context_changed context output as an object with entities --- apps/sim/triggers/slack/shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/triggers/slack/shared.ts b/apps/sim/triggers/slack/shared.ts index a9a3daa7f69..144cea94a09 100644 --- a/apps/sim/triggers/slack/shared.ts +++ b/apps/sim/triggers/slack/shared.ts @@ -130,7 +130,7 @@ export const SLACK_TRIGGER_OUTPUTS: Record = { context: { type: 'json', description: - 'What the user is currently viewing, as an entities array (each entry: type, value, team_id). Present for app_context_changed; null otherwise', + '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',