Preserve disabled Discord presentation buttons (#84312)

Summary:
- Adds `disabled` to the message presentation button schema, advertises Discord disabled-button support, prese ... through Discord component mapping and link serialization, and adds regression tests plus a changelog entry.
- Reproducibility: yes. Source inspection on current main shows `disabled` exists in the runtime type but is a ... rtised in Discord capabilities, dropped by adaptation, and omitted from Discord mapping/link serialization.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(discord): advertise disabled presentation support
- PR branch already contained follow-up commit before automerge: fix(discord): preserve disabled link buttons
- PR branch already contained follow-up commit before automerge: Preserve disabled Discord presentation buttons

Validation:
- ClawSweeper review passed for head 9bb60d8cbf.
- Required merge gates passed before the squash merge.

Prepared head SHA: 9bb60d8cbf
Review: https://github.com/openclaw/openclaw/pull/84312#issuecomment-4491983845

Co-authored-by: OpenClaw Contributor <100menotu001@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
clawsweeper[bot]
2026-05-19 23:29:48 +00:00
committed by GitHub
parent e61fe1c539
commit 97aa0c8c01
8 changed files with 133 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugins/hooks: apply a default 30-second timeout to `before_compaction` and `after_compaction` hooks so a hung plugin handler no longer blocks compaction completion. (#84153)
- Discord: preserve disabled presentation buttons when adapting and rendering Discord message controls. (#84188) Thanks @100menotu001.
- Plugins/perf: thread explicit plugin discovery results through `loadBundledCapabilityRuntimeRegistry`, `resolveBundledPluginSources`, and `listChannelCatalogEntries` so callers that already hold a discovery result skip redundant filesystem walks. Thanks @SebTardif.
- harden update restart script creation [AI]. (#84088) Thanks @pgondhi987.
- Docker: keep the bundled Codex plugin in official release image keep lists so the default OpenAI agent harness remains available after Docker pruning. Fixes #83613. (#83626) Thanks @YuanHanzhong.

View File

@@ -60,6 +60,7 @@ function createButtonComponent(params: {
class DynamicLinkButton extends LinkButton {
label = params.spec.label;
url = linkUrl;
override disabled = params.spec.disabled ?? false;
}
return { component: new DynamicLinkButton() };
}

View File

@@ -1,4 +1,4 @@
import { MessageFlags } from "discord-api-types/v10";
import { ButtonStyle, MessageFlags } from "discord-api-types/v10";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
let clearDiscordComponentEntries: typeof import("./components-registry.js").clearDiscordComponentEntries;
@@ -60,6 +60,41 @@ describe("discord components", () => {
expect(result.modals[0]?.allowedUsers).toEqual(["discord:user-1"]);
});
it("serializes disabled link buttons", () => {
const spec = readDiscordComponentSpec({
blocks: [
{
type: "actions",
buttons: [
{
label: "Open docs",
style: "link",
url: "https://example.com/docs",
disabled: true,
},
],
},
],
});
if (!spec) {
throw new Error("Expected component spec to be parsed");
}
const result = buildDiscordComponentMessage({ spec });
const serialized = result.components[0]?.serialize() as
| { components?: Array<{ components?: Array<Record<string, unknown>> }> }
| undefined;
const button = serialized?.components?.[0]?.components?.[0];
expect(button).toMatchObject({
label: "Open docs",
style: ButtonStyle.Link,
url: "https://example.com/docs",
disabled: true,
});
expect(result.entries).toHaveLength(0);
});
it("requires options for modal select fields", () => {
expect(() =>
readDiscordComponentSpec({

View File

@@ -1,3 +1,4 @@
import { adaptMessagePresentationForChannel } from "openclaw/plugin-sdk/interactive-runtime";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
createDiscordOutboundHoisted,
@@ -581,6 +582,56 @@ describe("discordOutbound", () => {
});
});
it("preserves disabled presentation buttons through channel adaptation", async () => {
const adaptedPresentation = adaptMessagePresentationForChannel({
capabilities: discordOutbound.presentationCapabilities,
presentation: {
blocks: [
{
type: "buttons",
buttons: [
{ label: "Already handled", value: "done", disabled: true },
{ label: "Open docs", url: "https://example.com/docs", disabled: true },
],
},
],
},
});
const payload = await discordOutbound.renderPresentation?.({
payload: { text: "Action state" },
presentation: adaptedPresentation,
ctx: {
cfg: {},
to: "channel:123456",
},
} as never);
if (!payload) {
throw new Error("expected Discord presentation payload");
}
const discordData = payload.channelData?.discord as
| { presentationComponents?: { blocks?: Array<{ type?: string; buttons?: unknown[] }> } }
| undefined;
const buttons = discordData?.presentationComponents?.blocks?.find(
(block) => block.type === "actions",
)?.buttons;
expect(buttons?.[0]).toEqual({
label: "Already handled",
style: "secondary",
callbackData: "done",
disabled: true,
});
expect(buttons?.[1]).toEqual({
label: "Open docs",
style: "link",
url: "https://example.com/docs",
disabled: true,
});
});
it("keeps replyToId on every internal component media send when replyToMode is all", async () => {
const payload = await discordOutbound.renderPresentation?.({
payload: {

View File

@@ -126,6 +126,7 @@ export const discordOutbound: ChannelOutboundAdapter = {
maxActionsPerRow: 5,
maxRows: 5,
maxLabelLength: 80,
supportsDisabled: true,
},
selects: {
maxOptions: 25,

View File

@@ -150,4 +150,40 @@ describe("buildDiscordInteractiveComponents", () => {
],
});
});
it("preserves disabled presentation buttons for Discord components", () => {
expect(
buildDiscordPresentationComponents({
blocks: [
{
type: "buttons",
buttons: [
{ label: "Already handled", value: "done", disabled: true },
{ label: "Open docs", url: "https://example.com/docs", disabled: true },
],
},
],
}),
).toEqual({
blocks: [
{
type: "actions",
buttons: [
{
label: "Already handled",
style: "secondary",
callbackData: "done",
disabled: true,
},
{
label: "Open docs",
style: "link",
url: "https://example.com/docs",
disabled: true,
},
],
},
],
});
});
});

View File

@@ -60,6 +60,9 @@ export function buildDiscordInteractiveComponents(
if (button.url) {
spec.url = button.url;
}
if (button.disabled === true) {
spec.disabled = true;
}
return spec;
}),
});
@@ -154,6 +157,9 @@ function appendDiscordPresentationButtonBlocks(
if (button.url) {
component.url = button.url;
}
if (button.disabled === true) {
component.disabled = true;
}
return component;
}),
});

View File

@@ -158,6 +158,7 @@ const presentationButtonSchema = Type.Object({
url: Type.Optional(Type.String()),
webApp: Type.Optional(Type.Object({ url: Type.String() })),
web_app: Type.Optional(Type.Object({ url: Type.String() })),
disabled: Type.Optional(Type.Boolean()),
style: Type.Optional(stringEnum(["primary", "secondary", "success", "danger"])),
});