mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-25 12:53:04 +00:00
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 head9bb60d8cbf. - Required merge gates passed before the squash merge. Prepared head SHA:9bb60d8cbfReview: 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:
@@ -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.
|
||||
|
||||
@@ -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() };
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -126,6 +126,7 @@ export const discordOutbound: ChannelOutboundAdapter = {
|
||||
maxActionsPerRow: 5,
|
||||
maxRows: 5,
|
||||
maxLabelLength: 80,
|
||||
supportsDisabled: true,
|
||||
},
|
||||
selects: {
|
||||
maxOptions: 25,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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"])),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user