From fee1cd986767bd08693d27f4b241319faacebc03 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 17 May 2026 12:07:31 +0100 Subject: [PATCH] docs: document presentation API surface --- .../.generated/plugin-sdk-api-baseline.sha256 | 4 ++-- extensions/slack/src/blocks-render.ts | 2 ++ extensions/telegram/src/button-types.ts | 2 ++ src/agents/runtime-plan/types.ts | 11 +++++++++ src/channels/plugins/outbound.types.ts | 23 +++++++++++++++++++ .../plugins/outbound/presentation-limits.ts | 8 +++++++ src/infra/exec-approval-reply.ts | 3 +++ src/interactive/payload.ts | 21 +++++++++++++++++ src/plugin-sdk/approval-renderers.ts | 2 ++ 9 files changed, 74 insertions(+), 2 deletions(-) diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index b1fa1b6c487..637ada49a17 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -df6c2799805dc3c57924dbb1632d11e7ed08ef4d7759f535998b170f1a10a638 plugin-sdk-api-baseline.json -e3526669b79e5eaa3b92e03bece552402209d3cf5b35343c33b62299f71b2efc plugin-sdk-api-baseline.jsonl +c8edc84c93c2077d8f37fd9c3626cfa5ea65d0d65f78899427e8251dfe9eac2e plugin-sdk-api-baseline.json +d5e2e3ebc8fc54a311f1da43c326bd5b883bbb5d99c2fec117955c887fcc3bac plugin-sdk-api-baseline.jsonl diff --git a/extensions/slack/src/blocks-render.ts b/extensions/slack/src/blocks-render.ts index 02479e09160..9819d4cce20 100644 --- a/extensions/slack/src/blocks-render.ts +++ b/extensions/slack/src/blocks-render.ts @@ -63,6 +63,7 @@ function readSlackOpenClawBlockIndex(blockId: string, prefix: string): number | return Number.isSafeInteger(value) && value > 0 ? value : undefined; } +/** Resolve existing OpenClaw Block Kit indexes so appended controls keep stable unique IDs. */ export function resolveSlackInteractiveBlockOffsets( blocks?: readonly SlackBlock[], ): SlackInteractiveBlockRenderOptions { @@ -189,6 +190,7 @@ export function buildSlackInteractiveBlocks( }).blocks; } +/** Render portable presentation blocks as Slack Block Kit blocks. */ export function buildSlackPresentationBlocks( presentation?: MessagePresentation, options: SlackInteractiveBlockRenderOptions = {}, diff --git a/extensions/telegram/src/button-types.ts b/extensions/telegram/src/button-types.ts index 59ba0f3bb23..b66b1e9c7dc 100644 --- a/extensions/telegram/src/button-types.ts +++ b/extensions/telegram/src/button-types.ts @@ -99,6 +99,7 @@ export function buildTelegramInteractiveButtons( return rows.length > 0 ? rows : undefined; } +/** Convert portable presentation controls to Telegram inline keyboard rows. */ export function buildTelegramPresentationButtons( presentation?: MessagePresentation, ): TelegramInlineButtons | undefined { @@ -122,6 +123,7 @@ export function buildTelegramPresentationButtons( return rows.length > 0 ? rows : undefined; } +/** Resolve Telegram inline buttons, preserving explicit and legacy button precedence. */ export function resolveTelegramInlineButtons(params: { buttons?: TelegramInlineButtons; presentation?: unknown; diff --git a/src/agents/runtime-plan/types.ts b/src/agents/runtime-plan/types.ts index edcb136b6df..ad14ecae5e4 100644 --- a/src/agents/runtime-plan/types.ts +++ b/src/agents/runtime-plan/types.ts @@ -82,15 +82,23 @@ export type AgentRuntimeProviderHandle = { export type AgentRuntimeInteractiveButtonStyle = "primary" | "secondary" | "success" | "danger"; +/** Portable action control exposed to agent runtime reply payloads. */ export type AgentRuntimeMessagePresentationButton = { + /** User-visible button label. */ label: string; + /** Callback command or opaque value sent when pressed. */ value?: string; + /** External URL opened by the button. */ url?: string; + /** Optional visual style hint for renderers that support styled actions. */ style?: AgentRuntimeInteractiveButtonStyle; }; +/** Portable select/menu option exposed to agent runtime reply payloads. */ export type AgentRuntimeMessagePresentationOption = { + /** User-visible option label. */ label: string; + /** Callback command or opaque value sent when selected. */ value: string; }; @@ -159,8 +167,11 @@ export type AgentRuntimeMessagePresentationBlock = }; export type AgentRuntimeMessagePresentation = { + /** Optional short heading rendered before blocks when supported. */ title?: string; + /** Optional severity/status tone for renderers that support toned presentations. */ tone?: AgentRuntimeMessagePresentationTone; + /** Ordered portable blocks rendered or downgraded by channel adapters. */ blocks: AgentRuntimeMessagePresentationBlock[]; }; diff --git a/src/channels/plugins/outbound.types.ts b/src/channels/plugins/outbound.types.ts index b2c97a02133..de6c50a3fec 100644 --- a/src/channels/plugins/outbound.types.ts +++ b/src/channels/plugins/outbound.types.ts @@ -42,31 +42,52 @@ export type ChannelOutboundPayloadContext = ChannelOutboundContext & { }; export type ChannelPresentationCapabilities = { + /** Whether the channel accepts structured presentation payloads at all. */ supported?: boolean; + /** Whether the channel can render button action blocks natively. */ buttons?: boolean; + /** Whether the channel can render select/menu blocks natively. */ selects?: boolean; + /** Whether the channel can render low-emphasis context blocks natively. */ context?: boolean; + /** Whether the channel can render divider blocks natively. */ divider?: boolean; + /** Per-channel limits used to adapt portable presentation blocks before rendering. */ limits?: { actions?: { + /** Maximum total button/select actions in one message. */ maxActions?: number; + /** Maximum buttons per rendered action row. */ maxActionsPerRow?: number; + /** Maximum action rows in one message. */ maxRows?: number; + /** Maximum user-visible button label length. */ maxLabelLength?: number; + /** Maximum callback/action value size in UTF-8 bytes. */ maxValueBytes?: number; + /** Whether action styles such as primary or danger are preserved. */ supportsStyles?: boolean; + /** Whether disabled button state is preserved. */ supportsDisabled?: boolean; + /** Whether priority/layout hints affect native rendering. */ supportsLayoutHints?: boolean; }; selects?: { + /** Maximum options in one select/menu block. */ maxOptions?: number; + /** Maximum user-visible option label length. */ maxLabelLength?: number; + /** Maximum option callback value size in UTF-8 bytes. */ maxValueBytes?: number; }; text?: { + /** Maximum text length for title, text, and context blocks. */ maxLength?: number; + /** Unit used by maxLength. Defaults to Unicode code points. */ encoding?: "characters" | "utf8-bytes" | "utf16-units"; + /** Markdown dialect understood by rendered text blocks. */ markdownDialect?: "plain" | "markdown" | "html" | "slack-mrkdwn" | "discord-markdown"; + /** Whether the channel can edit presentation text in-place. */ supportsEdit?: boolean; }; }; @@ -157,8 +178,10 @@ export type ChannelOutboundAdapter = { payload: ReplyPayload; results: readonly OutboundDeliveryResult[]; }) => Promise | void; + /** Channel-advertised presentation features and limits used by core adaptation. */ presentationCapabilities?: ChannelPresentationCapabilities; deliveryCapabilities?: ChannelDeliveryCapabilities; + /** Render an adapted portable presentation into channel-native payload data. */ renderPresentation?: (params: { payload: ReplyPayload; presentation: MessagePresentation; diff --git a/src/channels/plugins/outbound/presentation-limits.ts b/src/channels/plugins/outbound/presentation-limits.ts index d71d11da6d0..fab73c1b572 100644 --- a/src/channels/plugins/outbound/presentation-limits.ts +++ b/src/channels/plugins/outbound/presentation-limits.ts @@ -434,6 +434,12 @@ function adaptTextBlock( return block; } +/** + * Adapt a portable presentation to the target channel's advertised capabilities. + * + * Unsupported controls are downgraded to text/context fallback blocks where possible, and + * labels, values, rows, options, styles, disabled state, and text are clipped to channel limits. + */ export function adaptMessagePresentationForChannel(params: { presentation: MessagePresentation; capabilities?: ChannelPresentationCapabilities; @@ -508,6 +514,7 @@ export function adaptMessagePresentationForChannel(params: { }; } +/** Return the subset of buttons that can still be rendered under action limits. */ export function applyPresentationActionLimits( buttons: readonly MessagePresentationButton[], capabilities?: ChannelPresentationCapabilities, @@ -522,6 +529,7 @@ export function applyPresentationActionLimits( return block.flatMap((entry) => (entry.type === "buttons" ? entry.buttons : [])); } +/** Resolve an action page size that leaves room for reserved actions on the target channel. */ export function presentationPageSize( capabilities?: ChannelPresentationCapabilities, reservedActions = 0, diff --git a/src/infra/exec-approval-reply.ts b/src/infra/exec-approval-reply.ts index 99cf5b73286..6f97d9fe255 100644 --- a/src/infra/exec-approval-reply.ts +++ b/src/infra/exec-approval-reply.ts @@ -177,6 +177,7 @@ function buildApprovalPresentationButtons( })); } +/** Build the portable approval button presentation for already-resolved actions. */ export function buildApprovalPresentationFromActionDescriptors( actions: readonly ExecApprovalActionDescriptor[], ): MessagePresentation | undefined { @@ -184,6 +185,7 @@ export function buildApprovalPresentationFromActionDescriptors( return buttons.length > 0 ? { blocks: [{ type: "buttons", buttons }] } : undefined; } +/** Build the portable approval presentation for an approval id and decision allowlist. */ export function buildApprovalPresentation(params: { approvalId: string; ask?: string | null; @@ -198,6 +200,7 @@ export function buildApprovalPresentation(params: { ); } +/** Build the portable exec-approval presentation for command callback buttons. */ export function buildExecApprovalPresentation(params: { approvalCommandId: string; ask?: string | null; diff --git a/src/interactive/payload.ts b/src/interactive/payload.ts index a8f0d28224c..3eff5510403 100644 --- a/src/interactive/payload.ts +++ b/src/interactive/payload.ts @@ -5,14 +5,21 @@ import { export type InteractiveButtonStyle = "primary" | "secondary" | "success" | "danger"; +/** Visual tone for a portable message presentation. */ export type MessagePresentationTone = "info" | "success" | "warning" | "danger" | "neutral"; +/** Button style hint for renderers that support styled actions. */ export type MessagePresentationButtonStyle = InteractiveButtonStyle; +/** Portable action control rendered as a button or link by channel adapters. */ export type MessagePresentationButton = { + /** User-visible button label. */ label: string; + /** Callback command or opaque value sent when the button is pressed. */ value?: string; + /** External URL opened by the button instead of sending a callback value. */ url?: string; + /** Telegram-style web app launch target. */ webApp?: { url: string; }; @@ -22,13 +29,19 @@ export type MessagePresentationButton = { web_app?: { url: string; }; + /** Higher-priority buttons are kept first when channel limits require truncation. */ priority?: number; + /** Disable the button when the target channel supports disabled controls. */ disabled?: boolean; + /** Optional visual style hint; unsupported channels ignore or normalize it. */ style?: InteractiveButtonStyle; }; +/** Portable select/menu option. */ export type MessagePresentationOption = { + /** User-visible option label. */ label: string; + /** Callback command or opaque value sent when the option is selected. */ value: string; }; @@ -84,11 +97,13 @@ export type InteractiveReply = { export type MessagePresentationTextBlock = { type: "text"; + /** Primary markdown-ish text rendered in the message body. */ text: string; }; export type MessagePresentationContextBlock = { type: "context"; + /** Lower-emphasis contextual text, or normal text on channels without context support. */ text: string; }; @@ -98,12 +113,15 @@ export type MessagePresentationDividerBlock = { export type MessagePresentationButtonsBlock = { type: "buttons"; + /** Button row candidates; core may split or truncate them for channel limits. */ buttons: MessagePresentationButton[]; }; export type MessagePresentationSelectBlock = { type: "select"; + /** Optional prompt shown above or inside the select control. */ placeholder?: string; + /** Menu options; core may truncate them for channel limits. */ options: MessagePresentationOption[]; }; @@ -119,8 +137,11 @@ export type MessagePresentationBlock = | MessagePresentationSelectBlock; export type MessagePresentation = { + /** Optional short heading rendered before blocks when the channel supports it. */ title?: string; + /** Optional severity/status tone for renderers that support toned presentations. */ tone?: MessagePresentationTone; + /** Ordered portable blocks rendered or downgraded by the target channel adapter. */ blocks: MessagePresentationBlock[]; }; diff --git a/src/plugin-sdk/approval-renderers.ts b/src/plugin-sdk/approval-renderers.ts index 8e7b42da7cd..97a01d434cc 100644 --- a/src/plugin-sdk/approval-renderers.ts +++ b/src/plugin-sdk/approval-renderers.ts @@ -14,6 +14,7 @@ import type { ReplyPayload } from "./reply-payload.js"; const DEFAULT_ALLOWED_DECISIONS = ["allow-once", "allow-always", "deny"] as const; +/** Build a pending approval reply payload using the portable presentation API. */ export function buildApprovalPendingReplyPayload(params: { approvalKind?: "exec" | "plugin"; approvalId: string; @@ -46,6 +47,7 @@ export function buildApprovalPendingReplyPayload(params: { }; } +/** Build a resolved approval reply payload with approval metadata but no controls. */ export function buildApprovalResolvedReplyPayload(params: { approvalId: string; approvalSlug: string;