Rich messages and components
Build portable channel UI that renders as Slack Block Kit or Microsoft Teams Adaptive Cards, with explicit provider fallbacks.
Channels JSX lets application code describe a message once and lets the selected provider renderer turn it into native UI. The same component can produce Slack Block Kit or a Microsoft Teams Adaptive Card.
Enable Channels JSX#
Channels JSX does not use React at runtime. Point the TypeScript JSX transform
at @copilotkit/channels:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"jsx": "react-jsx",
"jsxImportSource": "@copilotkit/channels",
"strict": true,
"noEmit": true
},
"include": ["*.ts", "*.tsx"]
}Keep "type": "module" in package.json and use .tsx for files containing
JSX.
Build a portable status card#
import {
Context,
Divider,
Field,
Fields,
Header,
Message,
Section,
} from "@copilotkit/channels/ui";
export function IncidentCard(props: {
id: string;
summary: string;
owner: string;
status: string;
}) {
return (
<Message>
<Header>{`${props.id} ยท ${props.status}`}</Header>
<Section>{props.summary}</Section>
<Divider />
<Fields>
<Field>{`Owner: ${props.owner}`}</Field>
<Field>{`Status: ${props.status}`}</Field>
</Fields>
<Context>Updated from the incident service</Context>
</Message>
);
}Post the component from a message handler or tool:
channel.onMessage(async ({ thread }) => {
await thread.post(
<IncidentCard
id="INC-421"
summary="Checkout latency is above the SLO."
owner="Payments"
status="Investigating"
/>,
);
});Use named components with JSON-serializable props when they contain callbacks,
then register them in createChannel({ components: [...] }). Pure display
components do not need registration.
Know what each provider renders#
| Channels component | Slack | Microsoft Teams |
|---|---|---|
Message | Flattens to Block Kit; the current managed connector does not forward accent | Flattens into an Adaptive Card; accent has no native mapping |
Header | Header block | Large bold text block |
Section, Markdown | Slack mrkdwn section | Wrapped Adaptive Card text |
Fields, Field | Section fields; honors Field.label | Fact set; derives a label from Label: value text |
Context | Context block | Small, subtle text |
Actions, Button | Actions block and button | Top-level Action.Submit or Action.OpenUrl |
Select, Input | Native Block Kit controls | Adaptive Card inputs |
Image, Divider | Native image and divider blocks | Image and separator |
Table, Row, Cell | Native Slack table block | Adaptive Card 1.5 table |
Chart | Omitted by the Slack renderer | Teams chart extension where the client supports it |
Unknown or unsupported nodes are omitted instead of failing the whole message. That makes portable rendering resilient, but it also means the text around an optional visual must still carry the important result.
Select and Input have different interaction behavior by provider. Managed
Slack dispatches those controls directly. Managed Teams renders Adaptive Card
fields and submits them with a Button's Action.Submit; that Button callback
receives the other named fields in ctx.values. Teams does not dispatch
Select.onSelect or Input.onSubmit independently.
Teams renders Adaptive Card version 1.5. Chart nodes use Teams chart extensions; clients that do not support those extensions may omit the chart. Pair every chart with a text summary.
Button style is translated to positive or destructive when possible. Exact spacing, colors, and typography remain provider-owned.
Update a message safely#
thread.post() returns a MessageRef that can be updated during the current
managed delivery:
const ref = await thread.post(<IncidentCard {...incident} status="Loading" />);
await thread.update(ref, <IncidentCard {...incident} status="Investigating" />);Do not persist that ref and assume it remains updateable in a later delivery.
For a later button click, use the interaction's message.ref, check that
message.ref.id is non-empty, and treat the update as best-effort.
Managed Teams supports post, update, and delete frames.
Keep provider-specific payloads at the edge#
The { raw: nativePayload, provider } escape hatch is part of the low-level
renderable type, but it is not portable. Tag Slack Block Kit with provider: "slack" and a Teams Adaptive Card object with provider: "teams". A provider
mismatch fails explicitly before the effect reaches the provider. Prefer
Channels components in shared managed code.
Use interactive messages and approvals for callbacks, or browse the Channels UI reference for component props.