mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
docs(plugins): document message presentation cards
This commit is contained in:
@@ -69,7 +69,7 @@ Name lookup:
|
||||
- Channels: WhatsApp/Telegram/Discord/Google Chat/Slack/Mattermost (plugin)/Signal/iMessage/Matrix/Microsoft Teams
|
||||
- Required: `--target`, plus `--message`, `--media`, or `--presentation`
|
||||
- Optional: `--media`, `--presentation`, `--delivery`, `--pin`, `--reply-to`, `--thread-id`, `--gif-playback`, `--force-document`, `--silent`
|
||||
- Shared presentation payloads: `--presentation` sends semantic blocks (`text`, `context`, `divider`, `buttons`, `select`) that core renders through the selected channel's declared capabilities.
|
||||
- Shared presentation payloads: `--presentation` sends semantic blocks (`text`, `context`, `divider`, `buttons`, `select`) that core renders through the selected channel's declared capabilities. See [Message Presentation](/plugins/message-presentation).
|
||||
- Generic delivery preferences: `--delivery` accepts delivery hints such as `{ "pin": true }`; `--pin` is shorthand for pinned delivery when the channel supports it.
|
||||
- Telegram only: `--force-document` (send images and GIFs as documents to avoid Telegram compression)
|
||||
- Telegram only: `--thread-id` (forum topic id)
|
||||
@@ -214,7 +214,7 @@ openclaw message send --channel discord \
|
||||
--presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Approve","value":"approve","style":"success"},{"label":"Decline","value":"decline","style":"danger"}]}]}'
|
||||
```
|
||||
|
||||
Core renders the same `presentation` payload into Discord components, Slack blocks, Telegram inline buttons, Mattermost props, or Teams/Feishu cards depending on channel capability.
|
||||
Core renders the same `presentation` payload into Discord components, Slack blocks, Telegram inline buttons, Mattermost props, or Teams/Feishu cards depending on channel capability. See [Message Presentation](/plugins/message-presentation) for the full contract and fallback rules.
|
||||
|
||||
Send a richer presentation payload:
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ Implemented for the shared agent, CLI, plugin capability, and outbound delivery
|
||||
- Discord, Slack, Telegram, Mattermost, MS Teams, and Feishu renderers consume the generic contract.
|
||||
- Discord channel control-plane code no longer imports Carbon-backed UI containers.
|
||||
|
||||
Canonical docs now live in [Message Presentation](/plugins/message-presentation).
|
||||
Keep this plan as historical implementation context; update the canonical guide
|
||||
for contract, renderer, or fallback behavior changes.
|
||||
|
||||
## Problem
|
||||
|
||||
Channel UI is currently split across several incompatible surfaces:
|
||||
|
||||
@@ -1254,6 +1254,8 @@ Plugins should own channel-specific `describeMessageTool(...)` schema
|
||||
contributions for non-message primitives such as reactions, reads, and polls.
|
||||
Shared send presentation should use the generic `MessagePresentation` contract
|
||||
instead of provider-native button, component, block, or card fields.
|
||||
See [Message Presentation](/plugins/message-presentation) for the contract,
|
||||
fallback rules, provider mapping, and plugin author checklist.
|
||||
|
||||
Send-capable plugins declare what they can render through message capabilities:
|
||||
|
||||
|
||||
338
docs/plugins/message-presentation.md
Normal file
338
docs/plugins/message-presentation.md
Normal file
@@ -0,0 +1,338 @@
|
||||
---
|
||||
title: "Message Presentation"
|
||||
summary: "Semantic message cards, buttons, selects, fallback text, and delivery hints for channel plugins"
|
||||
read_when:
|
||||
- Adding or modifying message card, button, or select rendering
|
||||
- Building a channel plugin that supports rich outbound messages
|
||||
- Changing message tool presentation or delivery capabilities
|
||||
- Debugging provider-specific card/block/component rendering regressions
|
||||
---
|
||||
|
||||
# Message Presentation
|
||||
|
||||
Message presentation is OpenClaw's shared contract for rich outbound chat UI.
|
||||
It lets agents, CLI commands, approval flows, and plugins describe the message
|
||||
intent once, while each channel plugin renders the best native shape it can.
|
||||
|
||||
Use presentation for portable message UI:
|
||||
|
||||
- text sections
|
||||
- small context/footer text
|
||||
- dividers
|
||||
- buttons
|
||||
- select menus
|
||||
- card title and tone
|
||||
|
||||
Do not add new provider-native fields such as Discord `components`, Slack
|
||||
`blocks`, Telegram `buttons`, Teams `card`, or Feishu `card` to the shared
|
||||
message tool. Those are renderer outputs owned by the channel plugin.
|
||||
|
||||
## Contract
|
||||
|
||||
Plugin authors import the public contract from:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
MessagePresentation,
|
||||
ReplyPayloadDelivery,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
```
|
||||
|
||||
Shape:
|
||||
|
||||
```ts
|
||||
type MessagePresentation = {
|
||||
title?: string;
|
||||
tone?: "neutral" | "info" | "success" | "warning" | "danger";
|
||||
blocks: MessagePresentationBlock[];
|
||||
};
|
||||
|
||||
type MessagePresentationBlock =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "context"; text: string }
|
||||
| { type: "divider" }
|
||||
| { type: "buttons"; buttons: MessagePresentationButton[] }
|
||||
| { type: "select"; placeholder?: string; options: MessagePresentationOption[] };
|
||||
|
||||
type MessagePresentationButton = {
|
||||
label: string;
|
||||
value?: string;
|
||||
url?: string;
|
||||
style?: "primary" | "secondary" | "success" | "danger";
|
||||
};
|
||||
|
||||
type MessagePresentationOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type ReplyPayloadDelivery = {
|
||||
pin?:
|
||||
| boolean
|
||||
| {
|
||||
enabled: boolean;
|
||||
notify?: boolean;
|
||||
required?: boolean;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Button semantics:
|
||||
|
||||
- `value` is an application action value routed back through the channel's
|
||||
existing interaction path when the channel supports clickable controls.
|
||||
- `url` is a link button. It can exist without `value`.
|
||||
- `label` is required and is also used in text fallback.
|
||||
- `style` is advisory. Renderers should map unsupported styles to a safe
|
||||
default, not fail the send.
|
||||
|
||||
Select semantics:
|
||||
|
||||
- `options[].value` is the selected application value.
|
||||
- `placeholder` is advisory and may be ignored by channels without native
|
||||
select support.
|
||||
- If a channel does not support selects, fallback text lists the labels.
|
||||
|
||||
## Producer Examples
|
||||
|
||||
Simple card:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Deploy approval",
|
||||
"tone": "warning",
|
||||
"blocks": [
|
||||
{ "type": "text", "text": "Canary is ready to promote." },
|
||||
{ "type": "context", "text": "Build 1234, staging passed." },
|
||||
{
|
||||
"type": "buttons",
|
||||
"buttons": [
|
||||
{ "label": "Approve", "value": "deploy:approve", "style": "success" },
|
||||
{ "label": "Decline", "value": "deploy:decline", "style": "danger" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
URL-only link button:
|
||||
|
||||
```json
|
||||
{
|
||||
"blocks": [
|
||||
{ "type": "text", "text": "Release notes are ready." },
|
||||
{
|
||||
"type": "buttons",
|
||||
"buttons": [{ "label": "Open notes", "url": "https://example.com/release" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Select menu:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Choose environment",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "select",
|
||||
"placeholder": "Environment",
|
||||
"options": [
|
||||
{ "label": "Canary", "value": "env:canary" },
|
||||
{ "label": "Production", "value": "env:prod" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
CLI send:
|
||||
|
||||
```bash
|
||||
openclaw message send --channel slack \
|
||||
--target channel:C123 \
|
||||
--message "Deploy approval" \
|
||||
--presentation '{"title":"Deploy approval","tone":"warning","blocks":[{"type":"text","text":"Canary is ready."},{"type":"buttons","buttons":[{"label":"Approve","value":"deploy:approve","style":"success"},{"label":"Decline","value":"deploy:decline","style":"danger"}]}]}'
|
||||
```
|
||||
|
||||
Pinned delivery:
|
||||
|
||||
```bash
|
||||
openclaw message send --channel telegram \
|
||||
--target -1001234567890 \
|
||||
--message "Topic opened" \
|
||||
--pin
|
||||
```
|
||||
|
||||
Pinned delivery with explicit JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"pin": {
|
||||
"enabled": true,
|
||||
"notify": true,
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Renderer Contract
|
||||
|
||||
Channel plugins declare render support on their outbound adapter:
|
||||
|
||||
```ts
|
||||
const adapter: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
presentationCapabilities: {
|
||||
supported: true,
|
||||
buttons: true,
|
||||
selects: true,
|
||||
context: true,
|
||||
divider: true,
|
||||
},
|
||||
deliveryCapabilities: {
|
||||
pin: true,
|
||||
},
|
||||
renderPresentation({ payload, presentation, ctx }) {
|
||||
return renderNativePayload(payload, presentation, ctx);
|
||||
},
|
||||
async pinDeliveredMessage({ target, messageId, pin }) {
|
||||
await pinNativeMessage(target, messageId, { notify: pin.notify === true });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Capability fields are intentionally simple booleans. They describe what the
|
||||
renderer can make interactive, not every native platform limit. Renderers still
|
||||
own platform-specific limits such as maximum button count, block count, and
|
||||
card size.
|
||||
|
||||
## Core Render Flow
|
||||
|
||||
When a `ReplyPayload` or message action includes `presentation`, core:
|
||||
|
||||
1. Normalizes the presentation payload.
|
||||
2. Resolves the target channel's outbound adapter.
|
||||
3. Reads `presentationCapabilities`.
|
||||
4. Calls `renderPresentation` when the adapter can render the payload.
|
||||
5. Falls back to conservative text when the adapter is absent or cannot render.
|
||||
6. Sends the resulting payload through the normal channel delivery path.
|
||||
7. Applies delivery metadata such as `delivery.pin` after the first successful
|
||||
sent message.
|
||||
|
||||
Core owns fallback behavior so producers can stay channel-agnostic. Channel
|
||||
plugins own native rendering and interaction handling.
|
||||
|
||||
## Degradation Rules
|
||||
|
||||
Presentation must be safe to send on limited channels.
|
||||
|
||||
Fallback text includes:
|
||||
|
||||
- `title` as the first line
|
||||
- `text` blocks as normal paragraphs
|
||||
- `context` blocks as compact context lines
|
||||
- `divider` blocks as a visual separator
|
||||
- button labels, including URLs for link buttons
|
||||
- select option labels
|
||||
|
||||
Unsupported native controls should degrade rather than fail the whole send.
|
||||
Examples:
|
||||
|
||||
- Telegram with inline buttons disabled sends text fallback.
|
||||
- A channel without select support lists select options as text.
|
||||
- A URL-only button becomes either a native link button or a fallback URL line.
|
||||
- Optional pin failures do not fail the delivered message.
|
||||
|
||||
The main exception is `delivery.pin.required: true`; if pinning is requested as
|
||||
required and the channel cannot pin the sent message, delivery reports failure.
|
||||
|
||||
## Provider Mapping
|
||||
|
||||
Current bundled renderers:
|
||||
|
||||
| Channel | Native render target | Notes |
|
||||
| --------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Discord | Components and component containers | Preserves legacy `channelData.discord.components` for existing provider-native payload producers, but new shared sends should use `presentation`. |
|
||||
| Slack | Block Kit | Preserves legacy `channelData.slack.blocks` for existing provider-native payload producers, but new shared sends should use `presentation`. |
|
||||
| Telegram | Text plus inline keyboards | Buttons/selects require inline button capability for the target surface; otherwise text fallback is used. |
|
||||
| Mattermost | Text plus interactive props | Other blocks degrade to text. |
|
||||
| Microsoft Teams | Adaptive Cards | Plain `message` text is included with the card when both are provided. |
|
||||
| Feishu | Interactive cards | Card header can use `title`; body avoids duplicating that title. |
|
||||
| Plain channels | Text fallback | Channels without a renderer still get readable output. |
|
||||
|
||||
Provider-native payload compatibility is a transition affordance for existing
|
||||
reply producers. It is not a reason to add new shared native fields.
|
||||
|
||||
## Presentation vs InteractiveReply
|
||||
|
||||
`InteractiveReply` is the older internal subset used by approval and interaction
|
||||
helpers. It supports:
|
||||
|
||||
- text
|
||||
- buttons
|
||||
- selects
|
||||
|
||||
`MessagePresentation` is the canonical shared send contract. It adds:
|
||||
|
||||
- title
|
||||
- tone
|
||||
- context
|
||||
- divider
|
||||
- URL-only buttons
|
||||
- generic delivery metadata through `ReplyPayload.delivery`
|
||||
|
||||
Use helpers from `openclaw/plugin-sdk/interactive-runtime` when bridging older
|
||||
code:
|
||||
|
||||
```ts
|
||||
import {
|
||||
interactiveReplyToPresentation,
|
||||
normalizeMessagePresentation,
|
||||
presentationToInteractiveReply,
|
||||
renderMessagePresentationFallbackText,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
```
|
||||
|
||||
New code should accept or produce `MessagePresentation` directly.
|
||||
|
||||
## Delivery Pin
|
||||
|
||||
Pinning is delivery behavior, not presentation. Use `delivery.pin` instead of
|
||||
provider-native fields such as `channelData.telegram.pin`.
|
||||
|
||||
Semantics:
|
||||
|
||||
- `pin: true` pins the first successfully delivered message.
|
||||
- `pin.notify` defaults to `false`.
|
||||
- `pin.required` defaults to `false`.
|
||||
- Optional pin failures degrade and leave the sent message intact.
|
||||
- Required pin failures fail delivery.
|
||||
- Chunked messages pin the first delivered chunk, not the tail chunk.
|
||||
|
||||
Manual `pin`, `unpin`, and `pins` message actions still exist for existing
|
||||
messages where the provider supports those operations.
|
||||
|
||||
## Plugin Author Checklist
|
||||
|
||||
- Declare `presentation` from `describeMessageTool(...)` when the channel can
|
||||
render or safely degrade semantic presentation.
|
||||
- Add `presentationCapabilities` to the runtime outbound adapter.
|
||||
- Implement `renderPresentation` in runtime code, not control-plane plugin
|
||||
setup code.
|
||||
- Keep native UI libraries out of hot setup/catalog paths.
|
||||
- Preserve platform limits in the renderer and tests.
|
||||
- Add fallback tests for unsupported buttons, selects, URL buttons, title/text
|
||||
duplication, and mixed `message` plus `presentation` sends.
|
||||
- Add delivery pin support through `deliveryCapabilities.pin` and
|
||||
`pinDeliveredMessage` only when the provider can pin the sent message id.
|
||||
- Do not expose new provider-native card/block/component/button fields through
|
||||
the shared message action schema.
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Message CLI](/cli/message)
|
||||
- [Plugin SDK Overview](/plugins/sdk-overview)
|
||||
- [Plugin Architecture](/plugins/architecture#message-tool-schemas)
|
||||
- [Channel Presentation Refactor Plan](/plan/ui-channels)
|
||||
@@ -109,7 +109,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/allowlist-config-edit` | Allowlist config edit/read helpers |
|
||||
| `plugin-sdk/group-access` | Shared group-access decision helpers |
|
||||
| `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers |
|
||||
| `plugin-sdk/interactive-runtime` | Semantic message presentation, delivery, and legacy interactive reply helpers |
|
||||
| `plugin-sdk/interactive-runtime` | Semantic message presentation, delivery, and legacy interactive reply helpers. See [Message Presentation](/plugins/message-presentation) |
|
||||
| `plugin-sdk/channel-inbound` | Compatibility barrel for inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
|
||||
| `plugin-sdk/channel-mention-gating` | Narrow mention-policy helpers without the broader inbound runtime surface |
|
||||
| `plugin-sdk/channel-location` | Channel location context and formatting helpers |
|
||||
|
||||
Reference in New Issue
Block a user