.cursor/skills/add-channel-setup-guide/SKILL.md
Layer 1 = connecting the channel itself: the numbered stepper a developer follows to
create the provider app/bot, save its credentials in Novu, install/verify, and send a first
message — with a live "Listening… / Connected" indicator. ResolveAgentIntegrationGuide
renders this <Channel>SetupGuide as the setup view until the integration is connected,
then swaps to the connected/"what's next" view (separate add-channel-whats-next-onboarding skill).
Files live in apps/dashboard/src/components/agents/. Siblings: slack-setup-guide.tsx (quick/manual
modes + manifest), teams-setup-guide.tsx, telegram-setup-guide.tsx (simplest), whatsapp-setup-guide.tsx.
<Channel>SetupGuide
├─ useFetchIntegrations() → find integration by _id + providerId
├─ SetupStepperRail
│ └─ SetupStep × N → create app · save creds · install/verify
│ └─ SetupButton / IntegrationCredentialsSidebar trigger
├─ ListeningStatus → polls listAgentIntegrations every 1s;
│ fires onConnected + confetti when connectedAt is set
└─ IntegrationCredentialsSidebar → generic credential save (useUpdateIntegration)
Connection is detected, not asserted. A channel is "connected" only when the backend sets
connectedAt on the integration link (on the first real inbound message). Saving credentials or
finishing OAuth/install does not mean connected — keep those as separate local states.
All from setup-guide-primitives.tsx and setup-guide-step-utils.ts:
| Symbol | Role |
|---|---|
SetupStepperRail | Vertical numbered rail wrapping the steps column |
SetupStep | One step: index, status, title, description, rightContent?, extraContent?, fullWidthContent?, headerSlot?, dimmed?, sectionLabel?, inlineSectionLabel? |
SetupButton | Secondary outline action; href opens a new tab, else onClick; supports leadingIcon, disabled |
SetupModeToggle + SetupMode ('quick' | 'manual') | Optional dual-path setup (Slack) |
IntegrationCredentialsSidebar | Drawer that saves provider credentials via the generic integration form; onSaveSuccess, agentOnboarding |
ListeningStatus | Polls listAgentIntegrations; fires onConnected + confetti on connectedAt |
deriveStepStatus(i, firstIncomplete) | → 'completed' | 'current' | 'upcoming' |
hasIntegrationCredentials(credentials) | True once any string credential is saved |
ChatProviderIdEnum.<Channel> exists in @novu/shared, and the provider exists in packages/providers (brand-new providers are ask-first).integrationId).Copy the closest sibling, then rename. Full template: see reference.md.
apps/dashboard/src/components/agents/<channel>-setup-guide.tsx — export <Channel>SetupGuideagent-integration-guides/resolve-agent-integration-guide.tsx — in the setup switch, render <Channel>SetupGuide ... embedded /> and set setupDisplayName (the add-channel-whats-next-onboarding skill covers the rest of this resolver)@/api/agents or @/api/integrations only if the channel needs webhook registration / quick-setup / subscriber-link beyond a plain credential saveProps: { agent: AgentResponse; integrationId: string; stepOffset?: number; onStepsCompleted?: () => void; embedded?: boolean }
(stepOffset defaults to 1; Overview mounts the guide at a higher base, the Integrations detail page at 1).
State machine:
selectedIntegration from useFetchIntegrations() by _id === integrationId && providerId === ChatProviderIdEnum.<Channel>.hasIntegrationCredentials(...) ‖ a local credentialsSavedLocally, plus any install/connected flags. Reset all of it in useEffect([integrationId]).const base = stepOffset → compute firstIncompleteStep → deriveStepStatus(stepIndex, firstIncompleteStep) per SetupStep.SetupStepperRail, then ListeningStatus (in a pl-8 wrapper), then IntegrationCredentialsSidebar.ListeningStatus.onConnected → mark connected and call onStepsCompleted?.().embedded return (no Overview chrome — used by the resolver) and the standalone return.SetupButton href= the provider console. Optional: a manifest (CodeBlock, escape injected values) or a quick-setup input that calls a server action.SetupButton onClick={() => setIsCredentialsSidebarOpen(true)}; the sidebar's onSaveSuccess flips credentialsSavedLocally (and may trigger a provider action like webhook registration).ListeningStatus watches for connectedAt.connectedAt advances the final step (see the Slack guide's comments).getAgentIntegrationsQueryKey(currentEnvironment?._id, agent.identifier).integrationId changes so switching integrations doesn't leak progress.type (not interface) on the frontend, named exports, blank line before every return, no nested ternaries, animations from motion/react. Don't build/start the dashboard (port 4201) — check types via diagnostics.<Channel> integration: confirm steps advance as credentials save, the credential drawer opens/saves, and "Listening…" flips to "Connected" with confetti once a real message lands.