From 97aa0c8c010cb5b0d9bccab1f24e31dc8a0b2d08 Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 23:29:48 +0000 Subject: [PATCH] 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 9bb60d8cbf97064a271cd542e42d3be41ac50061. - Required merge gates passed before the squash merge. Prepared head SHA: 9bb60d8cbf97064a271cd542e42d3be41ac50061 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> --- CHANGELOG.md | 1 + extensions/discord/src/components.builders.ts | 1 + extensions/discord/src/components.test.ts | 37 +++++++++++++- .../discord/src/outbound-adapter.test.ts | 51 +++++++++++++++++++ extensions/discord/src/outbound-adapter.ts | 1 + .../discord/src/shared-interactive.test.ts | 36 +++++++++++++ extensions/discord/src/shared-interactive.ts | 6 +++ src/agents/tools/message-tool.ts | 1 + 8 files changed, 133 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e16983f3f2..e96173a7299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/discord/src/components.builders.ts b/extensions/discord/src/components.builders.ts index f36fc6c6447..62de8223cef 100644 --- a/extensions/discord/src/components.builders.ts +++ b/extensions/discord/src/components.builders.ts @@ -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() }; } diff --git a/extensions/discord/src/components.test.ts b/extensions/discord/src/components.test.ts index 8c05583c6a6..2b2689ed857 100644 --- a/extensions/discord/src/components.test.ts +++ b/extensions/discord/src/components.test.ts @@ -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> }> } + | 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({ diff --git a/extensions/discord/src/outbound-adapter.test.ts b/extensions/discord/src/outbound-adapter.test.ts index 524db51a78f..44b4fdc8835 100644 --- a/extensions/discord/src/outbound-adapter.test.ts +++ b/extensions/discord/src/outbound-adapter.test.ts @@ -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: { diff --git a/extensions/discord/src/outbound-adapter.ts b/extensions/discord/src/outbound-adapter.ts index c1f82f2916c..5f8dcd76905 100644 --- a/extensions/discord/src/outbound-adapter.ts +++ b/extensions/discord/src/outbound-adapter.ts @@ -126,6 +126,7 @@ export const discordOutbound: ChannelOutboundAdapter = { maxActionsPerRow: 5, maxRows: 5, maxLabelLength: 80, + supportsDisabled: true, }, selects: { maxOptions: 25, diff --git a/extensions/discord/src/shared-interactive.test.ts b/extensions/discord/src/shared-interactive.test.ts index 9b7cd8e27f0..9d543671c25 100644 --- a/extensions/discord/src/shared-interactive.test.ts +++ b/extensions/discord/src/shared-interactive.test.ts @@ -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, + }, + ], + }, + ], + }); + }); }); diff --git a/extensions/discord/src/shared-interactive.ts b/extensions/discord/src/shared-interactive.ts index bd97d6041a9..97e144585dc 100644 --- a/extensions/discord/src/shared-interactive.ts +++ b/extensions/discord/src/shared-interactive.ts @@ -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; }), }); diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index cb41a01135e..f33698a1cab 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -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"])), });