diff --git a/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx b/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx index 6760a73fd7a..70bde694d3d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx @@ -363,7 +363,7 @@ export function ImportCsvDialog({
- + {parsed.file.name} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx index 5f04b074361..b98b1c1f3bc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx @@ -1,11 +1,17 @@ -import { Checkbox, Label } from '@/components/emcn' +import { Info } from 'lucide-react' +import { Checkbox, Label, Tooltip } from '@/components/emcn' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +interface CheckboxListOption { + label: string + id: string + defaultChecked?: boolean + description?: string +} + interface CheckboxListProps { blockId: string - subBlockId: string - title: string - options: { label: string; id: string }[] + options: CheckboxListOption[] isPreview?: boolean subBlockValues?: Record disabled?: boolean @@ -13,36 +19,38 @@ interface CheckboxListProps { interface CheckboxItemProps { blockId: string - option: { label: string; id: string } + option: CheckboxListOption isPreview: boolean subBlockValues?: Record disabled: boolean } /** - * Individual checkbox item component that calls useSubBlockValue hook at top level + * Individual checkbox item component that calls useSubBlockValue hook at top level. + * + * @remarks + * A `null` store value means the user has never toggled the checkbox, in which + * case we fall back to `option.defaultChecked` for the displayed state. Any + * explicit boolean (including `false`) takes precedence over the default. */ function CheckboxItem({ blockId, option, isPreview, subBlockValues, disabled }: CheckboxItemProps) { - const [storeValue, setStoreValue] = useSubBlockValue(blockId, option.id) + const [storeValue, setStoreValue] = useSubBlockValue(blockId, option.id) - // Get preview value for this specific option const previewValue = isPreview && subBlockValues ? subBlockValues[option.id]?.value : undefined - - // Use preview value when in preview mode, otherwise use store value - const value = isPreview ? previewValue : storeValue + const rawValue = isPreview ? previewValue : storeValue + const effectiveValue = rawValue ?? option.defaultChecked ?? false const handleChange = (checked: boolean) => { - // Only update store when not in preview mode or disabled if (!isPreview && !disabled) { setStoreValue(checked) } } return ( -
+
@@ -52,21 +60,29 @@ function CheckboxItem({ blockId, option, isPreview, subBlockValues, disabled }: > {option.label} + {option.description && ( + + + + + +

{option.description}

+
+
+ )}
) } export function CheckboxList({ blockId, - subBlockId, - title, options, isPreview = false, subBlockValues, disabled = false, }: CheckboxListProps) { return ( -
+
{options.map((option) => ( >> = { + 'slack-setup-wizard': SlackSetupWizard, +} + +export type ModalId = keyof typeof MODAL_REGISTRY diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts new file mode 100644 index 00000000000..2caab15a639 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts @@ -0,0 +1 @@ +export { SlackSetupWizard } from './slack-setup-wizard' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx new file mode 100644 index 00000000000..f9ccbf6ed2e --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx @@ -0,0 +1,573 @@ +'use client' + +import { type ReactNode, useCallback, useMemo, useState } from 'react' +import { Check, ChevronRight, Clipboard, Info } from 'lucide-react' +import { useShallow } from 'zustand/react/shallow' +import { Checkbox, Input, Label, SecretInput, Tooltip, toast, Wizard } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import { useWebhookManagement } from '@/hooks/use-webhook-management' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { + buildSlackManifest, + SLACK_CAPABILITIES, + type SlackCapability, + type SlackCapabilityGroup, +} from '@/triggers/slack/capabilities' + +const DEFAULT_APP_NAME = 'Sim Workflow Bot' + +const GROUP_LABELS: Record = { + trigger: 'Triggers', + action: 'Actions', +} + +const GROUP_ORDER: readonly SlackCapabilityGroup[] = ['trigger', 'action'] as const + +const MODAL_HEIGHT_CLASS = 'h-[580px]' + +interface SlackSetupWizardProps { + blockId: string + isPreview?: boolean + disabled?: boolean +} + +/** + * Slack app setup wizard sub-block. + * + * @remarks + * The panel renders a single launcher button. The wizard lives in a modal + * with a fixed-height body so navigating between steps doesn't resize the + * dialog. Credentials are written directly into the sibling `signingSecret` + * and `botToken` sub-blocks via the shared sub-block store, so those fields + * in the panel are populated by the time the user clicks Done. + */ +export function SlackSetupWizard({ + blockId, + isPreview = false, + disabled = false, +}: SlackSetupWizardProps) { + const [open, setOpen] = useState(false) + const launcherDisabled = isPreview || disabled + + return ( + <> + + + + + ) +} + +interface WizardModalProps { + blockId: string + open: boolean + onOpenChange: (next: boolean) => void + isPreview: boolean + disabled: boolean +} + +function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: WizardModalProps) { + const [step, setStep] = useState(0) + + const { webhookUrl, isLoading } = useWebhookManagement({ + blockId, + triggerId: 'slack_webhook', + useWebhookUrl: true, + isPreview, + }) + + const [appName, setAppName] = useSubBlockValue(blockId, 'botDisplayName') + const [signingSecret, setSigningSecret] = useSubBlockValue(blockId, 'signingSecret') + const [botToken, setBotToken] = useSubBlockValue(blockId, 'botToken') + const selected = useCapabilitySelection(blockId) + + const displayAppName = appName ?? DEFAULT_APP_NAME + const effectiveWebhookUrl = !isLoading && webhookUrl ? webhookUrl : null + const canCopy = effectiveWebhookUrl !== null + const controlsDisabled = isPreview || disabled + + const manifestJson = useMemo(() => { + const manifest = buildSlackManifest(selected, { + appName: displayAppName.trim() || DEFAULT_APP_NAME, + webhookUrl: effectiveWebhookUrl, + }) + return JSON.stringify(manifest, null, 2) + }, [selected, displayAppName, effectiveWebhookUrl]) + + const handleOpenChange = useCallback( + (next: boolean) => { + if (!next) setStep(0) + onOpenChange(next) + }, + [onOpenChange] + ) + + return ( + + + { + if (!controlsDisabled) setAppName(v) + }} + selected={selected} + disabled={controlsDisabled} + /> + + + + + + { + if (!controlsDisabled) setSigningSecret(v) + }} + disabled={controlsDisabled} + /> + + + { + if (!controlsDisabled) setBotToken(v) + }} + disabled={controlsDisabled} + /> + + + + + + ) +} + +interface SubStepListProps { + children: ReactNode +} + +function SubStepList({ children }: SubStepListProps) { + return
    {children}
+} + +interface SubStepProps { + n: number + children: ReactNode +} + +function SubStep({ n, children }: SubStepProps) { + return ( +
  • + + {n} + +
    {children}
    +
  • + ) +} + +interface StepConfigureProps { + blockId: string + appName: string + onAppNameChange: (next: string) => void + selected: ReadonlySet + disabled: boolean +} + +function StepConfigure({ + blockId, + appName, + onAppNameChange, + selected, + disabled, +}: StepConfigureProps) { + return ( +
    +

    + Pick a name and choose what events should trigger your workflow and what actions your bot + can take. +

    +
    + + onAppNameChange(e.target.value)} + disabled={disabled} + placeholder={DEFAULT_APP_NAME} + className='h-9 text-sm' + /> +
    +
    + {GROUP_ORDER.map((group) => { + const items = SLACK_CAPABILITIES.filter((c) => c.group === group) + if (items.length === 0) return null + return ( + + ) + })} +
    +
    + ) +} + +interface StepCreateProps { + manifestJson: string + canCopy: boolean +} + +function StepCreate({ manifestJson, canCopy }: StepCreateProps) { + const [copied, setCopied] = useState(false) + + const handleCopy = useCallback(async () => { + if (!canCopy) return + try { + await navigator.clipboard.writeText(manifestJson) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + toast.error("Couldn't copy manifest — copy it manually from the developer console.") + } + }, [canCopy, manifestJson]) + + return ( +
    + + +
    Copy your manifest:
    + +
    + + Open the{' '} + + Slack Apps page + + . + + + Click Create New AppFrom a manifest and pick your + workspace. + + + Paste your manifest, then click NextCreate. + +
    +
    + ) +} + +interface StepSecretProps { + blockId: string + value: string + onChange: (next: string) => void + disabled: boolean +} + +function StepSecret({ blockId, value, onChange, disabled }: StepSecretProps) { + return ( +
    + + + In your new Slack app, open Basic Information. + + + Find Signing Secret and click Show, then copy it. + + Paste it into the field below. + + +
    + ) +} + +interface StepTokenProps { + blockId: string + value: string + onChange: (next: string) => void + disabled: boolean +} + +function StepToken({ blockId, value, onChange, disabled }: StepTokenProps) { + return ( +
    + + + In Slack, open Install AppInstall to Workspace and + authorize. + + + Copy the Bot User OAuth Token (starts with xoxb-). + + Paste it into the field below. + + +
    + ) +} + +interface SecretFieldProps { + id: string + label: string + value: string + onChange: (next: string) => void + disabled: boolean + placeholder?: string +} + +/** + * Label + SecretInput pair used by the signing-secret and bot-token wizard + * steps. The masked-on-blur behavior lives in the emcn `SecretInput` + * primitive; this wrapper just pins the label/input composition the wizard + * reuses twice. + */ +function SecretField({ id, label, value, onChange, disabled, placeholder }: SecretFieldProps) { + return ( +
    + + +
    + ) +} + +interface StepDoneProps { + hasSigningSecret: boolean + hasBotToken: boolean +} + +function StepDone({ hasSigningSecret, hasBotToken }: StepDoneProps) { + return ( +
    +

    + Your Slack app is set up. Save the workflow and Slack will verify the webhook URL + automatically. +

    + + + + + + + + Click Done and save this workflow. + +
    + ) +} + +interface StatusRowProps { + label: string + ok: boolean +} + +function StatusRow({ label, ok }: StatusRowProps) { + return ( + + + + {label} + {!ok && — not saved yet} + + + ) +} + +interface CapabilityGroupProps { + blockId: string + label: string + capabilities: readonly SlackCapability[] + selected: ReadonlySet + disabled: boolean +} + +function CapabilityGroup({ + blockId, + label, + capabilities, + selected, + disabled, +}: CapabilityGroupProps) { + return ( +
    +
    + {label} +
    +
    + {capabilities.map((c) => ( + + ))} +
    +
    + ) +} + +interface CapabilityRowProps { + blockId: string + capability: SlackCapability + checked: boolean + disabled: boolean +} + +function CapabilityRow({ blockId, capability, checked, disabled }: CapabilityRowProps) { + const [, setValue] = useSubBlockValue(blockId, capability.id) + const id = `${blockId}-wizard-${capability.id}` + + const handleChange = useCallback( + (next: boolean) => { + if (disabled) return + setValue(next) + }, + [disabled, setValue] + ) + + return ( +
    + handleChange(v === true)} + disabled={disabled} + /> + + + + + + +

    {capability.description}

    +
    +
    +
    + ) +} + +/** + * Builds the set of enabled capability ids by reading each capability's + * individual sub-block value in a single shallow-compared store selector. + * A `null`/`undefined` store value falls back to the capability's + * `defaultChecked` so untouched configs still reflect the defaults. + */ +function useCapabilitySelection(blockId: string): ReadonlySet { + const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) + const enabledFlags = useSubBlockStore( + useShallow((state) => { + const blockValues = activeWorkflowId + ? state.workflowValues[activeWorkflowId]?.[blockId] + : undefined + return SLACK_CAPABILITIES.map((c) => { + const raw = blockValues?.[c.id] + return typeof raw === 'boolean' ? raw : c.defaultChecked + }) + }) + ) + return useMemo( + () => new Set(SLACK_CAPABILITIES.filter((_, i) => enabledFlags[i]).map((c) => c.id)), + [enabledFlags] + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index d85adff7b7e..a354f177e08 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -50,6 +50,7 @@ import { VariablesInput, WorkflowSelectorInput, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' +import { MODAL_REGISTRY } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/modal-registry' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import type { SubBlockConfig } from '@/blocks/types' import { useWebhookManagement } from '@/hooks/use-webhook-management' @@ -815,9 +816,14 @@ function SubBlockComponent({ return ( ) + case 'modal': { + const ModalComponent = config.modalId ? MODAL_REGISTRY[config.modalId] : undefined + if (!ModalComponent) { + return ( +
    + Unknown modal: {String(config.modalId)} +
    + ) + } + return + } case 'messages-input': return ( { 'filter-builder', 'sort-builder', 'skill-input', + 'modal', ] const blocks = getAllBlocks() diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 0d1b204e03f..79242ff0037 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -162,6 +162,7 @@ export type SubBlockType = | 'text' // Read-only text display | 'router-input' // Router route definitions with descriptions | 'table-selector' // Table selector with link to view table + | 'modal' // Launches a modal component resolved via the client-side modal registry /** * Selector types that require display name hydration @@ -302,6 +303,8 @@ export interface SubBlockConfig { icon?: React.ComponentType<{ className?: string }> group?: string hidden?: boolean + defaultChecked?: boolean + description?: string }[] | (() => { label: string @@ -309,6 +312,8 @@ export interface SubBlockConfig { icon?: React.ComponentType<{ className?: string }> group?: string hidden?: boolean + defaultChecked?: boolean + description?: string }[]) min?: number max?: number @@ -325,6 +330,7 @@ export interface SubBlockConfig { hideWhenEnvSet?: string // Hide this subblock when the named NEXT_PUBLIC_ env var is truthy description?: string tooltip?: string // Tooltip text displayed via info icon next to the title + modalId?: string // Registry key when type is 'modal'; see sub-block/components/modal-registry.ts value?: (params: Record) => string grouped?: boolean scrollable?: boolean diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts index c001fbb77f5..70c7a6b383c 100644 --- a/apps/sim/components/emcn/components/index.ts +++ b/apps/sim/components/emcn/components/index.ts @@ -124,6 +124,7 @@ export { SModalTabsTrigger, SModalTrigger, } from './s-modal/s-modal' +export { SecretInput, type SecretInputProps } from './secret-input/secret-input' export { Skeleton } from './skeleton/skeleton' export { Slider, type SliderProps } from './slider/slider' export { Switch } from './switch/switch' @@ -159,3 +160,4 @@ export { type TourTooltipPlacement, type TourTooltipProps, } from './tour-tooltip/tour-tooltip' +export { Wizard, type WizardProps, type WizardStepProps } from './wizard/wizard' diff --git a/apps/sim/components/emcn/components/secret-input/secret-input.tsx b/apps/sim/components/emcn/components/secret-input/secret-input.tsx new file mode 100644 index 00000000000..923270e9983 --- /dev/null +++ b/apps/sim/components/emcn/components/secret-input/secret-input.tsx @@ -0,0 +1,69 @@ +/** + * A password-style input that masks its value with bullets only while the + * field is unfocused. + * + * @remarks + * Unlike a standard ``, this keeps the real text + * visible while the user is actively editing — so they can verify pasted + * secrets like signing tokens or API keys — and only swaps to bullets on + * blur. Uses plain `type="text"` so password managers don't auto-fill. + * + * @example + * ```tsx + * import { SecretInput } from '@/components/emcn' + * + * + * ``` + */ +'use client' + +import * as React from 'react' +import { Input, type InputProps } from '../input/input' + +export interface SecretInputProps + extends Omit { + /** Current value. Rendered as bullets when the input is not focused. */ + value: string + /** Called with the new value on every real edit (focused-only). */ + onChange: (next: string) => void +} + +const SecretInput = React.forwardRef( + ({ value, onChange, onFocus, onBlur, ...props }, ref) => { + const [isFocused, setIsFocused] = React.useState(false) + const displayValue = isFocused ? value : '•'.repeat(value.length) + + return ( + { + // Guard against synthetic change events (autofill, form reset) + // firing while blurred, which would otherwise overwrite the real + // value with bullet characters. + if (!isFocused) return + onChange(e.target.value) + }} + onFocus={(e) => { + setIsFocused(true) + onFocus?.(e) + }} + onBlur={(e) => { + setIsFocused(false) + onBlur?.(e) + }} + autoComplete='off' + {...props} + /> + ) + } +) +SecretInput.displayName = 'SecretInput' + +export { SecretInput } diff --git a/apps/sim/components/emcn/components/wizard/wizard.tsx b/apps/sim/components/emcn/components/wizard/wizard.tsx new file mode 100644 index 00000000000..ee68ee3742a --- /dev/null +++ b/apps/sim/components/emcn/components/wizard/wizard.tsx @@ -0,0 +1,194 @@ +'use client' + +import * as React from 'react' +import { cn } from '@/lib/core/utils/cn' +import { Button } from '../button/button' +import { + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + type ModalSize, +} from '../modal/modal' + +/** + * A multi-step modal wizard primitive. + * + * @remarks + * Wraps the emcn Modal with a step progress bar, a shared Back / Next / Done + * footer, and declarative `Wizard.Step` children. Step state is controlled + * from the outside so the consumer can hydrate from persisted state, reset + * on close, or jump around imperatively. + * + * @example + * ```tsx + * const [open, setOpen] = useState(false) + * const [step, setStep] = useState(0) + * + * { if (!next) setStep(0); setOpen(next) }} + * currentStep={step} + * onStepChange={setStep} + * size='lg' + * height='h-[580px]' + * > + * + * + * + * + * + * + * + * + * + * + * ``` + */ + +export interface WizardStepProps { + /** Title shown in the modal header when this step is active. */ + title: string + /** Step body. Rendered inside the modal body when this step is active. */ + children: React.ReactNode + /** + * When false, the Next button on this step is disabled. Lets the consumer + * gate progression on validation or async state. + * @default true + */ + canAdvance?: boolean +} + +/** + * Declares one step in the wizard. Carries metadata via props; the actual + * children are extracted and rendered by the `Wizard` root. + */ +const Step: React.FC = ({ children }) => <>{children} +Step.displayName = 'Wizard.Step' + +const STEP_DISPLAY_NAME = 'Wizard.Step' + +function isStepElement(node: React.ReactNode): node is React.ReactElement { + if (!React.isValidElement(node)) return false + const type = node.type as { displayName?: string } | string + return typeof type !== 'string' && type?.displayName === STEP_DISPLAY_NAME +} + +export interface WizardProps { + /** Whether the wizard modal is open. */ + open: boolean + /** Called when the modal's open state changes. */ + onOpenChange: (next: boolean) => void + /** Zero-indexed current step. */ + currentStep: number + /** Called with the new step index when the user clicks Back / Next. */ + onStepChange: (next: number) => void + /** + * Modal size variant. Matches `ModalContent` sizes. + * @default 'lg' + */ + size?: ModalSize + /** + * Optional fixed height for the modal content. Pass a Tailwind class + * (e.g. `h-[580px]`) to keep the modal a stable size across steps. + */ + height?: string + /** + * Called when the user clicks Done on the final step. Fires before the + * modal closes; the wizard will close the modal itself after. + */ + onComplete?: () => void + /** One or more `` elements. Non-step children are ignored. */ + children: React.ReactNode + /** Label for the Back button. @default 'Back' */ + backLabel?: string + /** Label for the Next button. @default 'Next' */ + nextLabel?: string + /** Label for the Done button on the final step. @default 'Done' */ + doneLabel?: string +} + +const WizardRoot: React.FC = ({ + open, + onOpenChange, + currentStep, + onStepChange, + size = 'lg', + height, + onComplete, + children, + backLabel = 'Back', + nextLabel = 'Next', + doneLabel = 'Done', +}) => { + const steps = React.Children.toArray(children).filter(isStepElement) + const total = steps.length + const clamped = Math.min(Math.max(0, currentStep), Math.max(0, total - 1)) + const activeStep = steps[clamped] + const canAdvance = activeStep?.props.canAdvance ?? true + const isLast = total > 0 && clamped === total - 1 + + const handleBack = React.useCallback(() => { + onStepChange(Math.max(0, clamped - 1)) + }, [clamped, onStepChange]) + + const handleNext = React.useCallback(() => { + onStepChange(Math.min(total - 1, clamped + 1)) + }, [clamped, total, onStepChange]) + + const handleDone = React.useCallback(() => { + onComplete?.() + onOpenChange(false) + }, [onComplete, onOpenChange]) + + if (total === 0) return null + + return ( + + + +
    + {activeStep?.props.title} + + Step {clamped + 1} of {total} + +
    +
    + +
    + {steps.map((_step, i) => ( +
    + ))} +
    + + {activeStep} + + + + {isLast ? ( + + ) : ( + + )} + + + + ) +} + +export const Wizard = Object.assign(WizardRoot, { + Step, +}) diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 95538651008..8a64b57a766 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -10,7 +10,7 @@ export interface ToolRuntimeSchemaEntry { } export const TOOL_RUNTIME_SCHEMAS: Record = { - ['agent']: { + agent: { parameters: { properties: { request: { @@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['auth']: { + auth: { parameters: { properties: { request: { @@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['check_deployment_status']: { + check_deployment_status: { parameters: { type: 'object', properties: { @@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['complete_job']: { + complete_job: { parameters: { type: 'object', properties: { @@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['context_write']: { + context_write: { parameters: { type: 'object', properties: { @@ -78,7 +78,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['crawl_website']: { + crawl_website: { parameters: { type: 'object', properties: { @@ -113,7 +113,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_file']: { + create_file: { parameters: { type: 'object', properties: { @@ -149,7 +149,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['create_folder']: { + create_folder: { parameters: { type: 'object', properties: { @@ -170,7 +170,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_job']: { + create_job: { parameters: { type: 'object', properties: { @@ -220,7 +220,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workflow']: { + create_workflow: { parameters: { type: 'object', properties: { @@ -245,7 +245,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workspace_mcp_server']: { + create_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -266,7 +266,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['debug']: { + debug: { parameters: { properties: { context: { @@ -285,7 +285,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_file']: { + delete_file: { parameters: { type: 'object', properties: { @@ -314,7 +314,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['delete_folder']: { + delete_folder: { parameters: { type: 'object', properties: { @@ -330,7 +330,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workflow']: { + delete_workflow: { parameters: { type: 'object', properties: { @@ -346,7 +346,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workspace_mcp_server']: { + delete_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -359,7 +359,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy']: { + deploy: { parameters: { properties: { request: { @@ -373,7 +373,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy_api']: { + deploy_api: { parameters: { type: 'object', properties: { @@ -447,7 +447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_chat']: { + deploy_chat: { parameters: { type: 'object', properties: { @@ -595,7 +595,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_mcp']: { + deploy_mcp: { parameters: { type: 'object', properties: { @@ -711,7 +711,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['deploymentType', 'deploymentStatus'], }, }, - ['download_to_workspace_file']: { + download_to_workspace_file: { parameters: { type: 'object', properties: { @@ -730,7 +730,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['edit_content']: { + edit_content: { parameters: { type: 'object', properties: { @@ -762,7 +762,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['edit_workflow']: { + edit_workflow: { parameters: { type: 'object', properties: { @@ -801,13 +801,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['file']: { + file: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['function_execute']: { + function_execute: { parameters: { type: 'object', properties: { @@ -868,7 +868,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_api_key']: { + generate_api_key: { parameters: { type: 'object', properties: { @@ -886,7 +886,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_image']: { + generate_image: { parameters: { type: 'object', properties: { @@ -923,7 +923,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_visualization']: { + generate_visualization: { parameters: { type: 'object', properties: { @@ -963,7 +963,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_outputs']: { + get_block_outputs: { parameters: { type: 'object', properties: { @@ -984,7 +984,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_upstream_references']: { + get_block_upstream_references: { parameters: { type: 'object', properties: { @@ -1006,7 +1006,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployed_workflow_state']: { + get_deployed_workflow_state: { parameters: { type: 'object', properties: { @@ -1019,7 +1019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployment_version']: { + get_deployment_version: { parameters: { type: 'object', properties: { @@ -1036,7 +1036,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_execution_summary']: { + get_execution_summary: { parameters: { type: 'object', properties: { @@ -1063,7 +1063,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_job_logs']: { + get_job_logs: { parameters: { type: 'object', properties: { @@ -1088,7 +1088,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_page_contents']: { + get_page_contents: { parameters: { type: 'object', properties: { @@ -1116,14 +1116,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_platform_actions']: { + get_platform_actions: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['get_workflow_data']: { + get_workflow_data: { parameters: { type: 'object', properties: { @@ -1142,7 +1142,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_workflow_logs']: { + get_workflow_logs: { parameters: { type: 'object', properties: { @@ -1168,7 +1168,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['glob']: { + glob: { parameters: { type: 'object', properties: { @@ -1187,7 +1187,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['grep']: { + grep: { parameters: { type: 'object', properties: { @@ -1234,7 +1234,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['job']: { + job: { parameters: { properties: { request: { @@ -1247,7 +1247,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge']: { + knowledge: { parameters: { properties: { request: { @@ -1260,7 +1260,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge_base']: { + knowledge_base: { parameters: { type: 'object', properties: { @@ -1452,7 +1452,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['list_folders']: { + list_folders: { parameters: { type: 'object', properties: { @@ -1464,14 +1464,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_user_workspaces']: { + list_user_workspaces: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['list_workspace_mcp_servers']: { + list_workspace_mcp_servers: { parameters: { type: 'object', properties: { @@ -1483,7 +1483,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_credential']: { + manage_credential: { parameters: { type: 'object', properties: { @@ -1512,7 +1512,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_custom_tool']: { + manage_custom_tool: { parameters: { type: 'object', properties: { @@ -1591,7 +1591,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_job']: { + manage_job: { parameters: { type: 'object', properties: { @@ -1661,7 +1661,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_mcp_tool']: { + manage_mcp_tool: { parameters: { type: 'object', properties: { @@ -1712,7 +1712,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_skill']: { + manage_skill: { parameters: { type: 'object', properties: { @@ -1744,7 +1744,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['materialize_file']: { + materialize_file: { parameters: { type: 'object', properties: { @@ -1778,7 +1778,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_folder']: { + move_folder: { parameters: { type: 'object', properties: { @@ -1796,7 +1796,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_workflow']: { + move_workflow: { parameters: { type: 'object', properties: { @@ -1816,7 +1816,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_get_auth_link']: { + oauth_get_auth_link: { parameters: { type: 'object', properties: { @@ -1830,7 +1830,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_request_access']: { + oauth_request_access: { parameters: { type: 'object', properties: { @@ -1844,7 +1844,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['open_resource']: { + open_resource: { parameters: { type: 'object', properties: { @@ -1872,7 +1872,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['read']: { + read: { parameters: { type: 'object', properties: { @@ -1899,7 +1899,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['redeploy']: { + redeploy: { parameters: { type: 'object', properties: { @@ -1967,7 +1967,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['rename_file']: { + rename_file: { parameters: { type: 'object', properties: { @@ -2002,7 +2002,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['rename_workflow']: { + rename_workflow: { parameters: { type: 'object', properties: { @@ -2019,7 +2019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['research']: { + research: { parameters: { properties: { topic: { @@ -2032,7 +2032,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['respond']: { + respond: { parameters: { additionalProperties: true, properties: { @@ -2055,7 +2055,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['restore_resource']: { + restore_resource: { parameters: { type: 'object', properties: { @@ -2073,7 +2073,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['revert_to_version']: { + revert_to_version: { parameters: { type: 'object', properties: { @@ -2090,7 +2090,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run']: { + run: { parameters: { properties: { context: { @@ -2107,7 +2107,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_block']: { + run_block: { parameters: { type: 'object', properties: { @@ -2139,7 +2139,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_from_block']: { + run_from_block: { parameters: { type: 'object', properties: { @@ -2171,7 +2171,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow']: { + run_workflow: { parameters: { type: 'object', properties: { @@ -2199,7 +2199,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow_until_block']: { + run_workflow_until_block: { parameters: { type: 'object', properties: { @@ -2231,7 +2231,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['scrape_page']: { + scrape_page: { parameters: { type: 'object', properties: { @@ -2252,7 +2252,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_documentation']: { + search_documentation: { parameters: { type: 'object', properties: { @@ -2269,7 +2269,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_library_docs']: { + search_library_docs: { parameters: { type: 'object', properties: { @@ -2290,7 +2290,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_online']: { + search_online: { parameters: { type: 'object', properties: { @@ -2331,7 +2331,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_patterns']: { + search_patterns: { parameters: { type: 'object', properties: { @@ -2353,7 +2353,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_block_enabled']: { + set_block_enabled: { parameters: { type: 'object', properties: { @@ -2375,7 +2375,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_environment_variables']: { + set_environment_variables: { parameters: { type: 'object', properties: { @@ -2409,7 +2409,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_global_workflow_variables']: { + set_global_workflow_variables: { parameters: { type: 'object', properties: { @@ -2447,7 +2447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['superagent']: { + superagent: { parameters: { properties: { task: { @@ -2461,7 +2461,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['table']: { + table: { parameters: { properties: { request: { @@ -2474,7 +2474,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['tool_search_tool_regex']: { + tool_search_tool_regex: { parameters: { properties: { case_insensitive: { @@ -2495,7 +2495,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_job_history']: { + update_job_history: { parameters: { type: 'object', properties: { @@ -2513,7 +2513,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_workspace_mcp_server']: { + update_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -2538,7 +2538,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_memory']: { + user_memory: { parameters: { type: 'object', properties: { @@ -2586,7 +2586,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_table']: { + user_table: { parameters: { type: 'object', properties: { @@ -2777,13 +2777,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['workflow']: { + workflow: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['workspace_file']: { + workspace_file: { parameters: { type: 'object', properties: { diff --git a/apps/sim/triggers/slack/capabilities.ts b/apps/sim/triggers/slack/capabilities.ts new file mode 100644 index 00000000000..1047b2f92dd --- /dev/null +++ b/apps/sim/triggers/slack/capabilities.ts @@ -0,0 +1,168 @@ +/** + * Slack app capabilities that can be toggled on in the manifest generator. + * + * @remarks + * Each capability maps to a set of bot OAuth scopes and bot events that must + * be declared in the Slack app manifest for the capability to work. The `id` + * is also used as the sub-block storage key (shape: `trigger_*` / `action_*`) + * so the same object serves as both a checkbox-list option and a manifest + * builder entry. See https://api.slack.com/reference/manifests. + */ + +export type SlackCapabilityGroup = 'trigger' | 'action' + +export interface SlackCapability { + id: string + label: string + description: string + defaultChecked: boolean + group: SlackCapabilityGroup + scopes: readonly string[] + events: readonly string[] +} + +export const SLACK_CAPABILITIES: readonly SlackCapability[] = [ + { + id: 'trigger_mention', + label: '@mention', + description: 'Trigger the workflow when someone @-mentions your bot.', + defaultChecked: true, + group: 'trigger', + scopes: ['app_mentions:read'], + events: ['app_mention'], + }, + { + id: 'trigger_dm', + label: 'Direct message', + description: 'Trigger the workflow when a user sends your bot a 1:1 direct message.', + defaultChecked: true, + group: 'trigger', + scopes: ['im:history', 'im:read'], + events: ['message.im'], + }, + { + id: 'trigger_group_dm', + label: 'Group direct message', + description: 'Trigger on messages in multi-person DMs your bot is part of.', + defaultChecked: true, + group: 'trigger', + scopes: ['mpim:history', 'mpim:read'], + events: ['message.mpim'], + }, + { + id: 'trigger_public_channel', + label: 'Public channel message', + description: 'Trigger on messages in public channels your bot has been invited to.', + defaultChecked: true, + group: 'trigger', + scopes: ['channels:history', 'channels:read'], + events: ['message.channels'], + }, + { + id: 'trigger_private_channel', + label: 'Private channel message', + description: 'Trigger on messages in private channels your bot has been invited to.', + defaultChecked: true, + group: 'trigger', + scopes: ['groups:history', 'groups:read'], + events: ['message.groups'], + }, + { + id: 'trigger_reaction', + label: 'Reaction', + description: + 'Trigger when an emoji reaction is added or removed anywhere the bot can see — public, private, or DM. Slack does not allow restricting the reactions scope by channel type.', + defaultChecked: true, + group: 'trigger', + scopes: ['reactions:read'], + events: ['reaction_added', 'reaction_removed'], + }, + { + id: 'action_send', + label: 'Send messages', + description: 'Let the bot post messages into channels it is a member of.', + defaultChecked: true, + group: 'action', + scopes: ['chat:write'], + events: [], + }, + { + id: 'action_add_reaction', + label: 'Add reactions', + description: 'Let the bot add emoji reactions to messages.', + defaultChecked: true, + group: 'action', + scopes: ['reactions:write'], + events: [], + }, + { + id: 'action_read_files', + label: 'Read file attachments', + description: 'Let the bot download file attachments on incoming messages.', + defaultChecked: true, + group: 'action', + scopes: ['files:read'], + events: [], + }, + { + id: 'action_read_users', + label: 'Look up users', + description: 'Resolve user IDs to names, profiles, and email addresses.', + defaultChecked: true, + group: 'action', + scopes: ['users:read', 'users:read.email'], + events: [], + }, +] as const + +const WEBHOOK_URL_PLACEHOLDER = '' + +export interface BuildManifestOptions { + appName: string + webhookUrl: string | null +} + +/** + * Builds a Slack app manifest object from a set of enabled capability ids. + * + * @remarks + * - Deduplicates scopes and events across overlapping capabilities. + * - Omits `settings.event_subscriptions` entirely when no events are selected — + * Slack's manifest validator rejects an empty `bot_events` array. + * - When `webhookUrl` is null, embeds a human-readable placeholder so the + * shape is visible before the workflow is deployed. + */ +export function buildSlackManifest( + enabled: ReadonlySet, + { appName, webhookUrl }: BuildManifestOptions +): Record { + const active = SLACK_CAPABILITIES.filter((c) => enabled.has(c.id)) + const scopes = [...new Set(active.flatMap((c) => c.scopes))].sort() + const events = [...new Set(active.flatMap((c) => c.events))].sort() + const displayName = appName.trim() || 'Sim Workflow Bot' + + const manifest: Record = { + display_information: { name: displayName }, + features: { + bot_user: { display_name: displayName, always_online: true }, + }, + oauth_config: { + scopes: { bot: scopes }, + }, + settings: { + org_deploy_enabled: false, + socket_mode_enabled: false, + token_rotation_enabled: false, + }, + } + + if (events.length > 0) { + const settings = manifest.settings as Record + settings.event_subscriptions = { + request_url: webhookUrl ?? WEBHOOK_URL_PLACEHOLDER, + bot_events: events, + } + } + + return manifest +} diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts index 72037b6bd82..c0d136aadbd 100644 --- a/apps/sim/triggers/slack/webhook.ts +++ b/apps/sim/triggers/slack/webhook.ts @@ -52,24 +52,11 @@ export const slackWebhookTrigger: TriggerConfig = { mode: 'trigger', }, { - id: 'triggerInstructions', - title: 'Setup Instructions', - type: 'text', - defaultValue: [ - 'Go to Slack Apps page', - 'If you don\'t have an app:
    • Create an app from scratch
    • Give it a name and select your workspace
    ', - 'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.', - 'Go to "OAuth & Permissions" and add bot token scopes:
    • app_mentions:read - For viewing messages that tag your bot with an @
    • chat:write - To send messages to channels your bot is a part of
    • files:read - To access files and images shared in messages
    • reactions:read - For listening to emoji reactions and fetching reacted-to message text
    ', - 'Go to "Event Subscriptions":
    • Enable events
    • Under "Subscribe to Bot Events", add app_mention to listen to messages that mention your bot
    • To receive all channel messages, add message.channels. For DMs add message.im, for group DMs add message.mpim, for private channels add message.groups
    • For reaction events, also add reaction_added and/or reaction_removed
    • Paste the Webhook URL above into the "Request URL" field
    ', - 'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.', - 'Copy the "Bot User OAuth Token" (starts with xoxb-) and paste it in the Bot Token field above to enable file downloads.', - 'Save changes in both Slack and here.', - ] - .map( - (instruction, index) => - `
    ${index + 1}. ${instruction}
    ` - ) - .join(''), + id: 'setupWizard', + title: 'Slack app setup', + type: 'modal', + modalId: 'slack-setup-wizard', + description: 'Walk through manifest creation, app install, and pasting credentials.', hideFromPreview: true, mode: 'trigger', },