Discord Rich & Interactive Messaging via discordjs-react
Context
ADR-0120 added thread-based Discord conversations. But the channel only sends plain text. Discord is an interactive application platform — embeds, buttons, select menus, stateful message updates. React components make an excellent substrate for composable, interactive Discord UIs. MCQs (multiple-choice questions) are the first concrete use case: the agent needs Joel to choose between options, and Discord renders that as interactive buttons that update in place.
discordjs-react provides a React reconciler for Discord — JSX components render to Discord messages with stateful re-rendering on interaction. useState in a button handler actually updates the Discord message.
Decision
New package: @joelclaw/discord-ui
Separate monorepo package (packages/discord-ui/) isolates React + react-reconciler + discordjs-react deps from the gateway’s core. Gateway imports via workspace dependency.
Two entry points:
@joelclaw/discord-ui— .ts API surface (no jsx required by consumer). Runtime init, async render helpers.@joelclaw/discord-ui/jsx— .tsx entry point for consumers with jsx configured. Direct component imports.@joelclaw/discord-ui/components— component library re-exports + discordjs-react primitives.
Component Library
Initial components:
- McqFlow — Sequential multiple-choice with buttons, recommended badges, auto-select timeout
- AgentResponse — Rich embed formatting for agent text output with optional action buttons
- StatusCard — Live-updating status embed (processing → done → error) with cancel button
- Approval — Yes/No confirmation with in-place update
- SelectPrompt — Dropdown selection menu with descriptions
Re-exports discordjs-react primitives (Embed, Button, ActionRow, Select, Option, Link) for custom components.
MCQ Adapter
Parallel to the existing Telegram MCQ adapter. When the agent calls the MCQ tool from a discord:* source:
- Discord MCQ adapter intercepts (via unified
withChannelMcqOverride) - Renders
McqFlowcomponent to the thread viarenderMcqToChannel() - User clicks buttons → React state updates → message edits in place
- Answers resolve back to the agent as tool result
Gateway Integration
initDiscordUI(client)called after Discord loginregisterDiscordMcqAdapter(fetchChannel)registers the adapter- Dynamic import of
@joelclaw/discord-uikeeps React out of gateway’s startup bundle - MCQ tool override renamed from
withTelegramMcqOverride→withChannelMcqOverride(handles both Telegram + Discord)
Implementation Plan
- New:
packages/discord-ui/— package with components, runtime, helpers - Modified:
packages/gateway/src/daemon.ts— init discord-ui, register adapter, unified MCQ override - Modified:
packages/gateway/src/channels/discord.ts— expose getClient(), fetchChannel() - New:
packages/gateway/src/commands/discord-mcq-adapter.ts— Discord MCQ adapter - Verification: @mention bot in Discord server → agent calls MCQ tool → interactive buttons appear in thread → clicking resolves answer back to agent
Non-goals
- Slash commands (we’re not a traditional bot)
- Public bot distribution
- Replacing Telegram channel (Discord is additive)
Consequences
- Discord becomes a first-class interactive UI surface
- MCQs render as interactive buttons instead of plain text
- Component library grows organically (loop monitors, task lists, dashboards)
- React + react-reconciler added as runtime deps (isolated to discord-ui package)
- Bun handles JSX natively at runtime; TypeScript boundary managed via .ts/.tsx split