Discord: add component v2 UI tool support (#17419)

This commit is contained in:
Shadow
2026-02-15 21:19:25 -06:00
committed by GitHub
parent b4a9eacd76
commit a61c2dc4bd
15 changed files with 2893 additions and 43 deletions

View File

@@ -1,6 +1,7 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { DiscordActionConfig } from "../../config/config.js";
import type { DiscordSendComponents, DiscordSendEmbeds } from "../../discord/send.shared.js";
import { readDiscordComponentSpec } from "../../discord/components.js";
import {
createThreadDiscord,
deleteMessageDiscord,
@@ -16,6 +17,7 @@ import {
removeOwnReactionsDiscord,
removeReactionDiscord,
searchMessagesDiscord,
sendDiscordComponentMessage,
sendMessageDiscord,
sendPollDiscord,
sendStickerDiscord,
@@ -233,24 +235,54 @@ export async function handleDiscordMessagingAction(
const to = readStringParam(params, "to", { required: true });
const asVoice = params.asVoice === true;
const silent = params.silent === true;
const rawComponents = params.components;
const componentSpec =
rawComponents && typeof rawComponents === "object" && !Array.isArray(rawComponents)
? readDiscordComponentSpec(rawComponents)
: null;
const components: DiscordSendComponents | undefined =
Array.isArray(rawComponents) || typeof rawComponents === "function"
? (rawComponents as DiscordSendComponents)
: undefined;
const content = readStringParam(params, "content", {
required: !asVoice,
required: !asVoice && !componentSpec && !components,
allowEmpty: true,
});
const mediaUrl =
readStringParam(params, "mediaUrl", { trim: false }) ??
readStringParam(params, "path", { trim: false }) ??
readStringParam(params, "filePath", { trim: false });
const filename = readStringParam(params, "filename");
const replyTo = readStringParam(params, "replyTo");
const rawComponents = params.components;
const components: DiscordSendComponents | undefined =
Array.isArray(rawComponents) || typeof rawComponents === "function"
? (rawComponents as DiscordSendComponents)
: undefined;
const rawEmbeds = params.embeds;
const embeds: DiscordSendEmbeds | undefined = Array.isArray(rawEmbeds)
? (rawEmbeds as DiscordSendEmbeds)
: undefined;
const sessionKey = readStringParam(params, "__sessionKey");
const agentId = readStringParam(params, "__agentId");
if (componentSpec) {
if (asVoice) {
throw new Error("Discord components cannot be sent as voice messages.");
}
if (embeds?.length) {
throw new Error("Discord components cannot include embeds.");
}
const normalizedContent = content?.trim() ? content : undefined;
const payload = componentSpec.text
? componentSpec
: { ...componentSpec, text: normalizedContent };
const result = await sendDiscordComponentMessage(to, payload, {
...(accountId ? { accountId } : {}),
silent,
replyTo: replyTo ?? undefined,
sessionKey: sessionKey ?? undefined,
agentId: agentId ?? undefined,
mediaUrl: mediaUrl ?? undefined,
filename: filename ?? undefined,
});
return jsonResult({ ok: true, result, components: true });
}
// Handle voice message sending
if (asVoice) {

View File

@@ -47,6 +47,98 @@ function buildRoutingSchema() {
};
}
const discordComponentEmojiSchema = Type.Object({
name: Type.String(),
id: Type.Optional(Type.String()),
animated: Type.Optional(Type.Boolean()),
});
const discordComponentOptionSchema = Type.Object({
label: Type.String(),
value: Type.String(),
description: Type.Optional(Type.String()),
emoji: Type.Optional(discordComponentEmojiSchema),
default: Type.Optional(Type.Boolean()),
});
const discordComponentButtonSchema = Type.Object({
label: Type.String(),
style: Type.Optional(stringEnum(["primary", "secondary", "success", "danger", "link"])),
url: Type.Optional(Type.String()),
emoji: Type.Optional(discordComponentEmojiSchema),
disabled: Type.Optional(Type.Boolean()),
});
const discordComponentSelectSchema = Type.Object({
type: Type.Optional(stringEnum(["string", "user", "role", "mentionable", "channel"])),
placeholder: Type.Optional(Type.String()),
minValues: Type.Optional(Type.Number()),
maxValues: Type.Optional(Type.Number()),
options: Type.Optional(Type.Array(discordComponentOptionSchema)),
});
const discordComponentBlockSchema = Type.Object({
type: Type.String(),
text: Type.Optional(Type.String()),
texts: Type.Optional(Type.Array(Type.String())),
accessory: Type.Optional(
Type.Object({
type: Type.String(),
url: Type.Optional(Type.String()),
button: Type.Optional(discordComponentButtonSchema),
}),
),
spacing: Type.Optional(stringEnum(["small", "large"])),
divider: Type.Optional(Type.Boolean()),
buttons: Type.Optional(Type.Array(discordComponentButtonSchema)),
select: Type.Optional(discordComponentSelectSchema),
items: Type.Optional(
Type.Array(
Type.Object({
url: Type.String(),
description: Type.Optional(Type.String()),
spoiler: Type.Optional(Type.Boolean()),
}),
),
),
file: Type.Optional(Type.String()),
spoiler: Type.Optional(Type.Boolean()),
});
const discordComponentModalFieldSchema = Type.Object({
type: Type.String(),
name: Type.Optional(Type.String()),
label: Type.String(),
description: Type.Optional(Type.String()),
placeholder: Type.Optional(Type.String()),
required: Type.Optional(Type.Boolean()),
options: Type.Optional(Type.Array(discordComponentOptionSchema)),
minValues: Type.Optional(Type.Number()),
maxValues: Type.Optional(Type.Number()),
minLength: Type.Optional(Type.Number()),
maxLength: Type.Optional(Type.Number()),
style: Type.Optional(stringEnum(["short", "paragraph"])),
});
const discordComponentModalSchema = Type.Object({
title: Type.String(),
triggerLabel: Type.Optional(Type.String()),
triggerStyle: Type.Optional(stringEnum(["primary", "secondary", "success", "danger", "link"])),
fields: Type.Array(discordComponentModalFieldSchema),
});
const discordComponentMessageSchema = Type.Object({
text: Type.Optional(Type.String()),
container: Type.Optional(
Type.Object({
accentColor: Type.Optional(Type.String()),
spoiler: Type.Optional(Type.Boolean()),
}),
),
blocks: Type.Optional(Type.Array(discordComponentBlockSchema)),
modal: Type.Optional(discordComponentModalSchema),
});
function buildSendSchema(options: { includeButtons: boolean; includeCards: boolean }) {
const props: Record<string, unknown> = {
message: Type.Optional(Type.String()),
@@ -105,6 +197,7 @@ function buildSendSchema(options: { includeButtons: boolean; includeCards: boole
},
),
),
components: Type.Optional(discordComponentMessageSchema),
};
if (!options.includeButtons) {
delete props.buttons;
@@ -481,6 +574,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
defaultAccountId: accountId ?? undefined,
gateway,
toolContext,
sessionKey: options?.agentSessionKey,
agentId: options?.agentSessionKey
? resolveSessionAgentId({ sessionKey: options.agentSessionKey, config: cfg })
: undefined,