- pi.md: 9 H2 + 14 H3 (Package Dependencies, File Structure, Tool Pipeline, etc.) - cli/hooks.md: 6 H2 (List All Hooks, Get Hook Information, etc.) - plugins/message-presentation.md: 8 H2 (Producer Examples, Renderer Contract, etc.) - plan/ui-channels.md: 7 H2 (Non Goals, Target Model, Refactor Steps, etc.) - install/ansible.md: 6 H2 + 1 H3 (What You Get, Quick Start, etc.) Mintlify anchor generation prefers sentence case for predictable URLs.
10 KiB
summary, title, read_when
| summary | title | read_when | |||
|---|---|---|---|---|---|
| Decouple semantic message presentation from channel native UI renderers. | Channel presentation refactor plan |
|
Status
Implemented for the shared agent, CLI, plugin capability, and outbound delivery surfaces:
ReplyPayload.presentationcarries semantic message UI.ReplyPayload.delivery.pincarries sent-message pin requests.- Shared message actions expose
presentation,delivery, andpininstead of provider-nativecomponents,blocks,buttons, orcard. - Core renders or auto-degrades presentation through plugin-declared outbound capabilities.
- 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. 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:
- Core owns a Discord-shaped cross-context renderer hook through
buildCrossContextComponents. - Discord
channel.tscan import native Carbon UI throughDiscordUiContainer, which pulls runtime UI dependencies into the channel plugin control plane. - The agent and CLI expose native payload escape hatches such as Discord
components, Slackblocks, Telegram or Mattermostbuttons, and Teams or Feishucard. ReplyPayload.channelDatacarries both transport hints and native UI envelopes.- The generic
interactivemodel exists, but it is narrower than the richer layouts already used by Discord, Slack, Teams, Feishu, LINE, Telegram, and Mattermost.
This makes core aware of native UI shapes, weakens plugin runtime laziness, and gives agents too many provider-specific ways to express the same message intent.
Goals
- Core decides the best semantic presentation for a message from declared capabilities.
- Extensions declare capabilities and render semantic presentation into native transport payloads.
- Web Control UI remains separate from chat native UI.
- Native channel payloads are not exposed through the shared agent or CLI message surface.
- Unsupported presentation features auto-degrade to the best text representation.
- Delivery behavior such as pinning a sent message is generic delivery metadata, not presentation.
Non goals
- No backwards compatibility shim for
buildCrossContextComponents. - No public native escape hatches for
components,blocks,buttons, orcard. - No core imports of channel-native UI libraries.
- No provider-specific SDK seams for bundled channels.
Target model
Add a core-owned presentation field to ReplyPayload.
type MessagePresentationTone = "neutral" | "info" | "success" | "warning" | "danger";
type MessagePresentation = {
tone?: MessagePresentationTone;
title?: string;
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;
};
interactive becomes a subset of presentation during migration:
interactivetext block maps topresentation.blocks[].type = "text".interactivebuttons block maps topresentation.blocks[].type = "buttons".interactiveselect block maps topresentation.blocks[].type = "select".
The external agent and CLI schemas now use presentation; interactive remains an internal legacy parser/rendering helper for existing reply producers.
Delivery metadata
Add a core-owned delivery field for send behavior that is not UI.
type ReplyPayloadDelivery = {
pin?:
| boolean
| {
enabled: boolean;
notify?: boolean;
required?: boolean;
};
};
Semantics:
delivery.pin = truemeans pin the first successfully delivered message.notifydefaults tofalse.requireddefaults tofalse; unsupported channels or failed pinning auto-degrade by continuing delivery.- Manual
pin,unpin, andlist-pinsmessage actions remain for existing messages.
Current Telegram ACP topic binding should move from channelData.telegram.pin = true to delivery.pin = true.
Runtime capability contract
Add presentation and delivery render hooks to the runtime outbound adapter, not the control-plane channel plugin.
type ChannelPresentationCapabilities = {
supported: boolean;
buttons?: boolean;
selects?: boolean;
context?: boolean;
divider?: boolean;
tones?: MessagePresentationTone[];
};
type ChannelDeliveryCapabilities = {
pinSentMessage?: boolean;
};
type ChannelOutboundAdapter = {
presentationCapabilities?: ChannelPresentationCapabilities;
renderPresentation?: (params: {
payload: ReplyPayload;
presentation: MessagePresentation;
ctx: ChannelOutboundSendContext;
}) => ReplyPayload | null;
deliveryCapabilities?: ChannelDeliveryCapabilities;
pinDeliveredMessage?: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
to: string;
threadId?: string | number | null;
messageId: string;
notify: boolean;
}) => Promise<void>;
};
Core behavior:
- Resolve target channel and runtime adapter.
- Ask for presentation capabilities.
- Degrade unsupported blocks before rendering.
- Call
renderPresentation. - If no renderer exists, convert presentation to text fallback.
- After successful send, call
pinDeliveredMessagewhendelivery.pinis requested and supported.
Channel mapping
Discord:
- Render
presentationto components v2 and Carbon containers in runtime-only modules. - Keep accent color helpers in light modules.
- Remove
DiscordUiContainerimports from channel plugin control-plane code.
Slack:
- Render
presentationto Block Kit. - Remove agent and CLI
blocksinput.
Telegram:
- Render text, context, and dividers as text.
- Render actions and select as inline keyboards when configured and allowed for the target surface.
- Use text fallback when inline buttons are disabled.
- Move ACP topic pinning to
delivery.pin.
Mattermost:
- Render actions as interactive buttons where configured.
- Render other blocks as text fallback.
MS Teams:
- Render
presentationto Adaptive Cards. - Keep manual pin/unpin/list-pins actions.
- Optionally implement
pinDeliveredMessageif Graph support is reliable for the target conversation.
Feishu:
- Render
presentationto interactive cards. - Keep manual pin/unpin/list-pins actions.
- Optionally implement
pinDeliveredMessagefor sent-message pinning if API behavior is reliable.
LINE:
- Render
presentationto Flex or template messages where possible. - Fall back to text for unsupported blocks.
- Remove LINE UI payloads from
channelData.
Plain or limited channels:
- Convert presentation to text with conservative formatting.
Refactor steps
- Reapply the Discord release fix that splits
ui-colors.tsfrom Carbon-backed UI and removesDiscordUiContainerfromextensions/discord/src/channel.ts. - Add
presentationanddeliverytoReplyPayload, outbound payload normalization, delivery summaries, and hook payloads. - Add
MessagePresentationschema and parser helpers in a narrow SDK/runtime subpath. - Replace message capabilities
buttons,cards,components, andblockswith semantic presentation capabilities. - Add runtime outbound adapter hooks for presentation render and delivery pinning.
- Replace cross-context component construction with
buildCrossContextPresentation. - Delete
src/infra/outbound/channel-adapters.tsand removebuildCrossContextComponentsfrom channel plugin types. - Change
maybeApplyCrossContextMarkerto attachpresentationinstead of native params. - Update plugin-dispatch send paths to consume only semantic presentation and delivery metadata.
- Remove agent and CLI native payload params:
components,blocks,buttons, andcard. - Remove SDK helpers that create native message-tool schemas, replacing them with presentation schema helpers.
- Remove UI/native envelopes from
channelData; keep only transport metadata until each remaining field is reviewed. - Migrate Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, and LINE renderers.
- Update docs for message CLI, channel pages, plugin SDK, and capability cookbook.
- Run import fanout profiling for Discord and affected channel entrypoints.
Steps 1-11 and 13-14 are implemented in this refactor for the shared agent, CLI, plugin capability, and outbound adapter contracts. Step 12 remains a deeper internal cleanup pass for provider-private channelData transport envelopes. Step 15 remains follow-up validation if we want quantified import-fanout numbers beyond the type/test gate.
Tests
Add or update:
- Presentation normalization tests.
- Presentation auto-degrade tests for unsupported blocks.
- Cross-context marker tests for plugin dispatch and core delivery paths.
- Channel render matrix tests for Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, LINE, and text fallback.
- Message tool schema tests proving native fields are gone.
- CLI tests proving native flags are gone.
- Discord entrypoint import-laziness regression covering Carbon.
- Delivery pin tests covering Telegram and generic fallback.
Open questions
- Should
delivery.pinbe implemented for Discord, Slack, MS Teams, and Feishu in the first pass, or only Telegram first? - Should
deliveryeventually absorb existing fields such asreplyToId,replyToCurrent,silent, andaudioAsVoice, or stay focused on post-send behaviors? - Should presentation support images or file references directly, or should media remain separate from UI layout for now?