docs/plan/ui-channels.md
Implemented for the shared agent, CLI, plugin capability, and outbound delivery surfaces:
ReplyPayload.presentation carries semantic message UI.ReplyPayload.delivery.pin carries sent-message pin requests.presentation, delivery, and pin instead of provider-native components, blocks, buttons, or card.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.
Channel UI is currently split across several incompatible surfaces:
buildCrossContextComponents.channel.ts can import native Carbon UI through DiscordUiContainer, which pulls runtime UI dependencies into the channel plugin control plane.components, Slack blocks, Telegram or Mattermost buttons, and Teams or Feishu card.ReplyPayload.channelData carries both transport hints and native UI envelopes.interactive model 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.
buildCrossContextComponents.components, blocks, buttons, or card.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:
interactive text block maps to presentation.blocks[].type = "text".interactive buttons block maps to presentation.blocks[].type = "buttons".interactive select block maps to presentation.blocks[].type = "select".The external agent and CLI schemas now use presentation; interactive remains an internal legacy parser/rendering helper for existing reply producers.
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 = true means pin the first successfully delivered message.notify defaults to false.required defaults to false; unsupported channels or failed pinning auto-degrade by continuing delivery.pin, unpin, and list-pins message actions remain for existing messages.Current Telegram ACP topic binding should move from channelData.telegram.pin = true to delivery.pin = true.
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:
renderPresentation.pinDeliveredMessage when delivery.pin is requested and supported.Discord:
presentation to components v2 and Carbon containers in runtime-only modules.DiscordUiContainer imports from channel plugin control-plane code.Slack:
presentation to Block Kit.blocks input.Telegram:
delivery.pin.Mattermost:
MS Teams:
presentation to Adaptive Cards.pinDeliveredMessage if Graph support is reliable for the target conversation.Feishu:
presentation to interactive cards.pinDeliveredMessage for sent-message pinning if API behavior is reliable.LINE:
presentation to Flex or template messages where possible.channelData.Plain or limited channels:
ui-colors.ts from Carbon-backed UI and removes DiscordUiContainer from extensions/discord/src/channel.ts.presentation and delivery to ReplyPayload, outbound payload normalization, delivery summaries, and hook payloads.MessagePresentation schema and parser helpers in a narrow SDK/runtime subpath.buttons, cards, components, and blocks with semantic presentation capabilities.buildCrossContextPresentation.src/infra/outbound/channel-adapters.ts and remove buildCrossContextComponents from channel plugin types.maybeApplyCrossContextMarker to attach presentation instead of native params.components, blocks, buttons, and card.channelData; keep only transport metadata until each remaining field is reviewed.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.
Add or update:
delivery.pin be implemented for Discord, Slack, MS Teams, and Feishu in the first pass, or only Telegram first?delivery eventually absorb existing fields such as replyToId, replyToCurrent, silent, and audioAsVoice, or stay focused on post-send behaviors?