Skip to content

Commit ad861d4

Browse files
committed
feat: add presentation capability limits
1 parent 868315a commit ad861d4

23 files changed

Lines changed: 1662 additions & 29 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
1515
- Agents/skills: tighten bundled skill prompts and metadata, quote skill descriptions, refresh current CLI/API guidance, and update embedded sherpa-onnx runtime downloads.
1616
- Skills: update the Obsidian skill to target the official `obsidian` CLI and require its registered binary instead of the third-party `obsidian-cli`.
1717
- Skills: add a Python debugging skill for pdb, breakpoint(), post-mortem inspection, and debugpy remote attach.
18+
- Plugins/messages: add presentation capability limits for channel renderers, adapt rich message controls before native rendering, and mark legacy `interactive`/Slack directive producer APIs as deprecated.
1819
- Proxy: support HTTPS managed forward-proxy endpoints and scoped `proxy.tls.caFile` CA trust for proxy endpoint TLS. (#79171) Thanks @jesse-merhi.
1920
- QA-Lab: add first-hour 20-turn and optional 100-turn runtime parity scenarios, with tier metadata for standard and soak QA gates. (#80323) Thanks @100yenadmin.
2021
- QA-Lab: add a live-only Codex Pi-shaped Read vocabulary canary so runtime parity catches native workspace-read prompt compatibility drift. (#80323) Thanks @100yenadmin.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
701ff598b929acfe779b728fe5660aac13e317526117baad80eef6bd1c5663c3 plugin-sdk-api-baseline.json
2-
4bb4bf89bb568ece9c8adbb0126f77cabc33698003190f272d23a2266d6bff84 plugin-sdk-api-baseline.jsonl
1+
ec9fa02c3af9c210f7dbf6157d2f18e5c7171c29e6ae13b4f539aefcb25d178a plugin-sdk-api-baseline.json
2+
2ee394b924edb9843987710a65f9d45523efadd7e7940e01c88ea39dcdcdad7c plugin-sdk-api-baseline.jsonl

docs/channels/slack.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,9 @@ Slash sessions use isolated keys like `agent:<agentId>:slack:slash:<userId>` and
11991199
## Interactive replies
12001200

12011201
Slack can render agent-authored interactive reply controls, but this feature is disabled by default.
1202+
For new agent, CLI, and plugin output, prefer the shared
1203+
`presentation` buttons or select blocks. They use the same Slack interaction
1204+
path while also degrading on other channels.
12021205

12031206
Enable it globally:
12041207

@@ -1232,16 +1235,20 @@ Or enable it for one Slack account only:
12321235
}
12331236
```
12341237

1235-
When enabled, agents can emit Slack-only reply directives:
1238+
When enabled, agents can still emit deprecated Slack-only reply directives:
12361239

12371240
- `[[slack_buttons: Approve:approve, Reject:reject]]`
12381241
- `[[slack_select: Choose a target | Canary:canary, Production:production]]`
12391242

1240-
These directives compile into Slack Block Kit and route clicks or selections back through the existing Slack interaction event path.
1243+
These directives compile into Slack Block Kit and route clicks or selections
1244+
back through the existing Slack interaction event path. Keep them for old
1245+
prompts and Slack-specific escape hatches; use shared presentation for new
1246+
portable controls.
12411247

12421248
Notes:
12431249

1244-
- This is Slack-specific UI. Other channels do not translate Slack Block Kit directives into their own button systems.
1250+
- This is Slack-specific legacy UI. Other channels do not translate Slack Block
1251+
Kit directives into their own button systems.
12451252
- The interactive callback values are OpenClaw-generated opaque tokens, not raw agent-authored values.
12461253
- If generated interactive blocks would exceed Slack Block Kit limits, OpenClaw falls back to the original text reply instead of sending an invalid blocks payload.
12471254

docs/cli/message.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,12 @@ Send a Telegram Mini App button through generic presentation:
288288

289289
```
290290
openclaw message send --channel telegram --target 123456789 --message "Open app:" \
291-
--presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Launch","web_app":{"url":"https://example.com/app"}}]}]}'
291+
--presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Launch","webApp":{"url":"https://example.com/app"}}]}]}'
292292
```
293293

294-
Telegram `web_app` buttons are supported only in private chats between a user
295-
and the bot.
294+
Telegram web app buttons are supported only in private chats between a user and
295+
the bot. Older JSON payloads using `web_app` still parse, but `webApp` is the
296+
canonical presentation field.
296297

297298
Send a Teams card through generic presentation:
298299

docs/plan/ui-channels.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ type MessagePresentationOption = {
9090
- `interactive` select block maps to `presentation.blocks[].type = "select"`.
9191

9292
The external agent and CLI schemas now use `presentation`; `interactive` remains an internal legacy parser/rendering helper for existing reply producers.
93+
The public producer-facing API treats `interactive` as deprecated. Runtime
94+
support remains so existing approval helpers and older plugins continue to
95+
work while new code emits `presentation`.
9396

9497
## Delivery metadata
9598

@@ -128,6 +131,29 @@ type ChannelPresentationCapabilities = {
128131
context?: boolean;
129132
divider?: boolean;
130133
tones?: MessagePresentationTone[];
134+
limits?: {
135+
actions?: {
136+
maxActions?: number;
137+
maxActionsPerRow?: number;
138+
maxRows?: number;
139+
maxLabelLength?: number;
140+
maxValueBytes?: number;
141+
supportsStyles?: boolean;
142+
supportsDisabled?: boolean;
143+
supportsLayoutHints?: boolean;
144+
};
145+
selects?: {
146+
maxOptions?: number;
147+
maxLabelLength?: number;
148+
maxValueBytes?: number;
149+
};
150+
text?: {
151+
maxLength?: number;
152+
encoding?: "characters" | "utf8-bytes" | "utf16-units";
153+
markdownDialect?: "plain" | "markdown" | "html" | "slack-mrkdwn" | "discord-markdown";
154+
supportsEdit?: boolean;
155+
};
156+
};
131157
};
132158

133159
type ChannelDeliveryCapabilities = {
@@ -160,7 +186,8 @@ Core behavior:
160186

161187
- Resolve target channel and runtime adapter.
162188
- Ask for presentation capabilities.
163-
- Degrade unsupported blocks before rendering.
189+
- Degrade unsupported blocks and apply generic capability limits before
190+
rendering.
164191
- Call `renderPresentation`.
165192
- If no renderer exists, convert presentation to text fallback.
166193
- After successful send, call `pinDeliveredMessage` when `delivery.pin` is requested and supported.

docs/plugins/message-presentation.md

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ type MessagePresentationButton = {
5757
value?: string;
5858
url?: string;
5959
webApp?: { url: string };
60+
/** @deprecated Use webApp. Accepted for legacy JSON payloads only. */
6061
web_app?: { url: string };
62+
priority?: number;
63+
disabled?: boolean;
6164
style?: "primary" | "secondary" | "success" | "danger";
6265
};
6366

@@ -82,11 +85,19 @@ Button semantics:
8285
- `value` is an application action value routed back through the channel's
8386
existing interaction path when the channel supports clickable controls.
8487
- `url` is a link button. It can exist without `value`.
85-
- `webApp` and `web_app` describe a channel-native web app button. Telegram
86-
renders this as `web_app` and only supports it in private chats.
88+
- `webApp` describes a channel-native web app button. Telegram renders this
89+
as `web_app` and only supports it in private chats. `web_app` is still
90+
accepted in loose JSON payloads for compatibility, but TypeScript producers
91+
should use `webApp`.
8792
- `label` is required and is also used in text fallback.
8893
- `style` is advisory. Renderers should map unsupported styles to a safe
8994
default, not fail the send.
95+
- `priority` is optional. When a channel advertises action limits and controls
96+
must be dropped, core keeps higher-priority buttons first and preserves
97+
original order among equal priority buttons. When all controls fit, authored
98+
order is preserved.
99+
- `disabled` is optional. Channels must opt in with `supportsDisabled`; otherwise
100+
core degrades the disabled control to non-interactive fallback text.
90101

91102
Select semantics:
92103

@@ -205,6 +216,27 @@ const adapter: ChannelOutboundAdapter = {
205216
selects: true,
206217
context: true,
207218
divider: true,
219+
limits: {
220+
actions: {
221+
maxActions: 25,
222+
maxActionsPerRow: 5,
223+
maxRows: 5,
224+
maxLabelLength: 80,
225+
maxValueBytes: 100,
226+
supportsStyles: true,
227+
supportsDisabled: false,
228+
},
229+
selects: {
230+
maxOptions: 25,
231+
maxLabelLength: 100,
232+
maxValueBytes: 100,
233+
},
234+
text: {
235+
maxLength: 2000,
236+
encoding: "characters",
237+
markdownDialect: "discord-markdown",
238+
},
239+
},
208240
},
209241
deliveryCapabilities: {
210242
pin: true,
@@ -218,10 +250,49 @@ const adapter: ChannelOutboundAdapter = {
218250
};
219251
```
220252

221-
Capability fields are intentionally simple booleans. They describe what the
222-
renderer can make interactive, not every native platform limit. Renderers still
223-
own platform-specific limits such as maximum button count, block count, and
224-
card size.
253+
Capability booleans describe what the renderer can make interactive. Optional
254+
`limits` describe the generic envelope core can adapt before calling the
255+
renderer:
256+
257+
```ts
258+
type ChannelPresentationCapabilities = {
259+
supported?: boolean;
260+
buttons?: boolean;
261+
selects?: boolean;
262+
context?: boolean;
263+
divider?: boolean;
264+
limits?: {
265+
actions?: {
266+
maxActions?: number;
267+
maxActionsPerRow?: number;
268+
maxRows?: number;
269+
maxLabelLength?: number;
270+
maxValueBytes?: number;
271+
supportsStyles?: boolean;
272+
supportsDisabled?: boolean;
273+
supportsLayoutHints?: boolean;
274+
};
275+
selects?: {
276+
maxOptions?: number;
277+
maxLabelLength?: number;
278+
maxValueBytes?: number;
279+
};
280+
text?: {
281+
maxLength?: number;
282+
encoding?: "characters" | "utf8-bytes" | "utf16-units";
283+
markdownDialect?: "plain" | "markdown" | "html" | "slack-mrkdwn" | "discord-markdown";
284+
supportsEdit?: boolean;
285+
};
286+
};
287+
};
288+
```
289+
290+
Core applies generic limits to semantic controls before rendering. Renderers
291+
still own final provider-specific validation and clipping for native block
292+
count, card size, URL limits, and provider quirks that cannot be expressed in
293+
the generic contract. If limits remove every control from a block, core keeps
294+
the labels as non-interactive context text so the delivered message still has a
295+
visible fallback.
225296

226297
## Core render flow
227298

@@ -230,10 +301,12 @@ When a `ReplyPayload` or message action includes `presentation`, core:
230301
1. Normalizes the presentation payload.
231302
2. Resolves the target channel's outbound adapter.
232303
3. Reads `presentationCapabilities`.
233-
4. Calls `renderPresentation` when the adapter can render the payload.
234-
5. Falls back to conservative text when the adapter is absent or cannot render.
235-
6. Sends the resulting payload through the normal channel delivery path.
236-
7. Applies delivery metadata such as `delivery.pin` after the first successful
304+
4. Applies generic capability limits such as action count, label length, and
305+
select option count when the adapter advertises them.
306+
5. Calls `renderPresentation` when the adapter can render the payload.
307+
6. Falls back to conservative text when the adapter is absent or cannot render.
308+
7. Sends the resulting payload through the normal channel delivery path.
309+
8. Applies delivery metadata such as `delivery.pin` after the first successful
237310
sent message.
238311

239312
Core owns fallback behavior so producers can stay channel-agnostic. Channel
@@ -303,15 +376,20 @@ code:
303376

304377
```ts
305378
import {
379+
adaptMessagePresentationForChannel,
380+
applyPresentationActionLimits,
306381
interactiveReplyToPresentation,
307382
normalizeMessagePresentation,
383+
presentationPageSize,
308384
presentationToInteractiveControlsReply,
309385
presentationToInteractiveReply,
310386
renderMessagePresentationFallbackText,
311387
} from "openclaw/plugin-sdk/interactive-runtime";
312388
```
313389

314-
New code should accept or produce `MessagePresentation` directly.
390+
New code should accept or produce `MessagePresentation` directly. Existing
391+
`interactive` payloads are a deprecated subset of `presentation`; runtime
392+
support remains for older producers.
315393

316394
`presentationToInteractiveReply(...)` preserves visible presentation text by
317395
mapping the title, text, context, buttons, and selects into the older
@@ -351,7 +429,9 @@ messages where the provider supports those operations.
351429
- Implement `renderPresentation` in runtime code, not control-plane plugin
352430
setup code.
353431
- Keep native UI libraries out of hot setup/catalog paths.
354-
- Preserve platform limits in the renderer and tests.
432+
- Declare generic capability limits on `presentationCapabilities.limits` when
433+
they are known.
434+
- Preserve final platform limits in the renderer and tests.
355435
- Add fallback tests for unsupported buttons, selects, URL buttons, title/text
356436
duplication, and mixed `message` plus `presentation` sends.
357437
- Add delivery pin support through `deliveryCapabilities.pin` and

extensions/discord/src/outbound-adapter.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,24 @@ export const discordOutbound: ChannelOutboundAdapter = {
120120
selects: true,
121121
context: true,
122122
divider: true,
123+
limits: {
124+
actions: {
125+
maxActions: 25,
126+
maxActionsPerRow: 5,
127+
maxRows: 5,
128+
maxLabelLength: 80,
129+
},
130+
selects: {
131+
maxOptions: 25,
132+
maxLabelLength: 100,
133+
maxValueBytes: 100,
134+
},
135+
text: {
136+
maxLength: DISCORD_TEXT_CHUNK_LIMIT,
137+
encoding: "characters",
138+
markdownDialect: "discord-markdown",
139+
},
140+
},
123141
},
124142
deliveryCapabilities: {
125143
durableFinal: {

extensions/feishu/src/channel.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,19 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
13871387
selects: false,
13881388
context: true,
13891389
divider: true,
1390+
limits: {
1391+
actions: {
1392+
maxActions: 20,
1393+
maxActionsPerRow: 5,
1394+
maxLabelLength: 40,
1395+
maxValueBytes: 1024,
1396+
},
1397+
text: {
1398+
maxLength: 4000,
1399+
encoding: "characters",
1400+
markdownDialect: "markdown",
1401+
},
1402+
},
13901403
},
13911404
renderPresentation: async (ctx) => {
13921405
const runtime = await loadFeishuChannelRuntime();

extensions/feishu/src/outbound.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,19 @@ export const feishuOutbound: ChannelOutboundAdapter = {
514514
selects: false,
515515
context: true,
516516
divider: true,
517+
limits: {
518+
actions: {
519+
maxActions: 20,
520+
maxActionsPerRow: 5,
521+
maxLabelLength: 40,
522+
maxValueBytes: 1024,
523+
},
524+
text: {
525+
maxLength: 4000,
526+
encoding: "characters",
527+
markdownDialect: "markdown",
528+
},
529+
},
517530
},
518531
renderPresentation: renderFeishuPresentationPayload,
519532
sendPayload: async (ctx) => {

extensions/matrix/src/channel.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,14 @@ const matrixChannelOutbound: ChannelOutboundAdapter = {
341341
selects: true,
342342
context: true,
343343
divider: true,
344+
limits: {
345+
text: {
346+
maxLength: 4000,
347+
encoding: "characters",
348+
markdownDialect: "markdown",
349+
supportsEdit: true,
350+
},
351+
},
344352
},
345353
shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload }) =>
346354
shouldSuppressLocalMatrixExecApprovalPrompt({

0 commit comments

Comments
 (0)