docs: document presentation API surface

This commit is contained in:
Peter Steinberger
2026-05-17 12:07:31 +01:00
parent ee72ce8cf7
commit fee1cd9867
9 changed files with 74 additions and 2 deletions

View File

@@ -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

View File

@@ -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 = {},

View File

@@ -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;

View File

@@ -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[];
};

View File

@@ -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> | 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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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[];
};

View File

@@ -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;