From 37ffd3463b8b334e51fe38b25b80f66bf885702c Mon Sep 17 00:00:00 2001 From: Solvely-Colin <211764741+Solvely-Colin@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:08:59 +0000 Subject: [PATCH 1/2] discord: support forum post creation via thread-create Allow thread-create to create Discord forum posts by accepting initial content and appliedTagIds, and using POST /channels/{id}/threads. --- src/agents/tools/discord-actions-messaging.ts | 17 ++++++++++++-- .../plugins/actions/discord/handle-action.ts | 6 +++++ src/discord/send.messages.ts | 22 ++++++++++++++++++- src/discord/send.types.ts | 7 ++++++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/agents/tools/discord-actions-messaging.ts b/src/agents/tools/discord-actions-messaging.ts index f90fb60de..562baa50e 100644 --- a/src/agents/tools/discord-actions-messaging.ts +++ b/src/agents/tools/discord-actions-messaging.ts @@ -279,18 +279,31 @@ export async function handleDiscordMessagingAction( const channelId = resolveChannelId(); const name = readStringParam(params, "name", { required: true }); const messageId = readStringParam(params, "messageId"); + // Optional initial content (required for forum posts). + const content = readStringParam(params, "content") ?? undefined; + // Optional applied tag ids (forum posts). + const appliedTagIds = Array.isArray(params.appliedTagIds) + ? params.appliedTagIds.filter((x) => typeof x === "string" && x.trim()) + : undefined; const autoArchiveMinutesRaw = params.autoArchiveMinutes; const autoArchiveMinutes = typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw) ? autoArchiveMinutesRaw : undefined; + const thread = accountId ? await createThreadDiscord( channelId, - { name, messageId, autoArchiveMinutes }, + { name, messageId, autoArchiveMinutes, content, appliedTagIds }, { accountId }, ) - : await createThreadDiscord(channelId, { name, messageId, autoArchiveMinutes }); + : await createThreadDiscord(channelId, { + name, + messageId, + autoArchiveMinutes, + content, + appliedTagIds, + }); return jsonResult({ ok: true, thread }); } case "threadList": { diff --git a/src/channels/plugins/actions/discord/handle-action.ts b/src/channels/plugins/actions/discord/handle-action.ts index 90e95d14d..94189eef0 100644 --- a/src/channels/plugins/actions/discord/handle-action.ts +++ b/src/channels/plugins/actions/discord/handle-action.ts @@ -180,6 +180,10 @@ export async function handleDiscordMessageAction( if (action === "thread-create") { const name = readStringParam(params, "threadName", { required: true }); const messageId = readStringParam(params, "messageId"); + // Optional initial post content (required for forum post creation). + const content = readStringParam(params, "message"); + // Optional forum tag ids. + const appliedTagIds = readStringArrayParam(params, "appliedTagIds"); const autoArchiveMinutes = readNumberParam(params, "autoArchiveMin", { integer: true, }); @@ -190,6 +194,8 @@ export async function handleDiscordMessageAction( channelId: resolveChannelId(), name, messageId, + content, + appliedTagIds, autoArchiveMinutes, }, cfg, diff --git a/src/discord/send.messages.ts b/src/discord/send.messages.ts index 93bc378d7..b859f1674 100644 --- a/src/discord/send.messages.ts +++ b/src/discord/send.messages.ts @@ -94,10 +94,30 @@ export async function createThreadDiscord( ) { const rest = resolveDiscordRest(opts); const body: Record = { name: payload.name }; + if (payload.autoArchiveMinutes) { body.auto_archive_duration = payload.autoArchiveMinutes; } - const route = Routes.threads(channelId, payload.messageId); + + // Forum posts (channel type 15) require an initial message payload. + // If content is provided, create the thread with an initial post. + if (payload.content) { + body.message = { content: payload.content }; + if (Array.isArray(payload.appliedTagIds) && payload.appliedTagIds.length) { + body.applied_tags = payload.appliedTagIds; + } + // NOTE: discord-api-types Routes doesn't currently expose a helper for + // POST /channels/{channel.id}/threads (it only exposes listing archived threads), + // so use the raw route string. + const route = `/channels/${channelId}/threads`; + return await rest.post(route, { body }); + } + + // Regular threads: create from an existing message when messageId is provided, + // otherwise create a standard thread in the channel. + const route = payload.messageId + ? Routes.threads(channelId, payload.messageId) + : `/channels/${channelId}/threads`; return await rest.post(route, { body }); } diff --git a/src/discord/send.types.ts b/src/discord/send.types.ts index 5ea63366a..edb901a4f 100644 --- a/src/discord/send.types.ts +++ b/src/discord/send.types.ts @@ -67,9 +67,16 @@ export type DiscordMessageEdit = { }; export type DiscordThreadCreate = { + /** Optional message id to start a thread from (for classic threads). */ messageId?: string; + /** Thread / forum post title. */ name: string; + /** Auto-archive duration in minutes. */ autoArchiveMinutes?: number; + /** Optional initial post content (required for forum post creation). */ + content?: string; + /** Optional forum tag ids (applied tags). */ + appliedTagIds?: string[]; }; export type DiscordThreadList = { From ca2ab303ba8614054d06d66e810fec22a1346b0c Mon Sep 17 00:00:00 2001 From: Solvely-Colin <211764741+Solvely-Colin@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:12:06 +0000 Subject: [PATCH 2/2] discord: allow embeds/components in message tool send --- src/agents/tools/discord-actions-messaging.ts | 5 ++++ src/agents/tools/message-tool.ts | 24 +++++++++++++++++++ .../plugins/actions/discord/handle-action.ts | 2 ++ src/discord/send.outbound.ts | 3 +++ src/discord/send.shared.ts | 6 +++++ 5 files changed, 40 insertions(+) diff --git a/src/agents/tools/discord-actions-messaging.ts b/src/agents/tools/discord-actions-messaging.ts index 562baa50e..31b3ff002 100644 --- a/src/agents/tools/discord-actions-messaging.ts +++ b/src/agents/tools/discord-actions-messaging.ts @@ -233,11 +233,16 @@ export async function handleDiscordMessagingAction( const replyTo = readStringParam(params, "replyTo"); const embeds = Array.isArray(params.embeds) && params.embeds.length > 0 ? params.embeds : undefined; + const components = + Array.isArray(params.components) && params.components.length > 0 + ? params.components + : undefined; const result = await sendMessageDiscord(to, content, { ...(accountId ? { accountId } : {}), mediaUrl, replyTo, embeds, + components, }); return jsonResult({ ok: true, result }); } diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 4ea178a54..4fc457b40 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -87,6 +87,30 @@ function buildSendSchema(options: { includeButtons: boolean; includeCards: boole }, ), ), + embeds: Type.Optional( + Type.Array( + Type.Object( + {}, + { + additionalProperties: true, + description: + "Provider-specific embed objects (Discord embeds, etc.). Passed through to the channel adapter when supported.", + }, + ), + ), + ), + components: Type.Optional( + Type.Array( + Type.Object( + {}, + { + additionalProperties: true, + description: + "Provider-specific message components (Discord buttons/selects, etc.). Passed through when supported.", + }, + ), + ), + ), }; if (!options.includeButtons) delete props.buttons; if (!options.includeCards) delete props.card; diff --git a/src/channels/plugins/actions/discord/handle-action.ts b/src/channels/plugins/actions/discord/handle-action.ts index 94189eef0..f4f06be27 100644 --- a/src/channels/plugins/actions/discord/handle-action.ts +++ b/src/channels/plugins/actions/discord/handle-action.ts @@ -37,6 +37,7 @@ export async function handleDiscordMessageAction( const mediaUrl = readStringParam(params, "media", { trim: false }); const replyTo = readStringParam(params, "replyTo"); const embeds = Array.isArray(params.embeds) ? params.embeds : undefined; + const components = Array.isArray(params.components) ? params.components : undefined; return await handleDiscordAction( { action: "sendMessage", @@ -46,6 +47,7 @@ export async function handleDiscordMessageAction( mediaUrl: mediaUrl ?? undefined, replyTo: replyTo ?? undefined, embeds, + components, }, cfg, ); diff --git a/src/discord/send.outbound.ts b/src/discord/send.outbound.ts index a47d0f4f1..c42d9c809 100644 --- a/src/discord/send.outbound.ts +++ b/src/discord/send.outbound.ts @@ -29,6 +29,7 @@ type DiscordSendOpts = { replyTo?: string; retry?: RetryConfig; embeds?: unknown[]; + components?: unknown[]; }; export async function sendMessageDiscord( @@ -63,6 +64,7 @@ export async function sendMessageDiscord( request, accountInfo.config.maxLinesPerMessage, opts.embeds, + opts.components, chunkMode, ); } else { @@ -74,6 +76,7 @@ export async function sendMessageDiscord( request, accountInfo.config.maxLinesPerMessage, opts.embeds, + opts.components, chunkMode, ); } diff --git a/src/discord/send.shared.ts b/src/discord/send.shared.ts index 4919be29d..26d975657 100644 --- a/src/discord/send.shared.ts +++ b/src/discord/send.shared.ts @@ -232,6 +232,7 @@ async function sendDiscordText( request: DiscordRequest, maxLinesPerMessage?: number, embeds?: unknown[], + components?: unknown[], chunkMode?: ChunkMode, ) { if (!text.trim()) { @@ -252,6 +253,7 @@ async function sendDiscordText( content: chunks[0], message_reference: messageReference, ...(embeds?.length ? { embeds } : {}), + ...(components?.length ? { components } : {}), }, }) as Promise<{ id: string; channel_id: string }>, "text", @@ -268,6 +270,7 @@ async function sendDiscordText( content: chunk, message_reference: isFirst ? messageReference : undefined, ...(isFirst && embeds?.length ? { embeds } : {}), + ...(isFirst && components?.length ? { components } : {}), }, }) as Promise<{ id: string; channel_id: string }>, "text", @@ -289,6 +292,7 @@ async function sendDiscordMedia( request: DiscordRequest, maxLinesPerMessage?: number, embeds?: unknown[], + components?: unknown[], chunkMode?: ChunkMode, ) { const media = await loadWebMedia(mediaUrl); @@ -309,6 +313,7 @@ async function sendDiscordMedia( content: caption || undefined, message_reference: messageReference, ...(embeds?.length ? { embeds } : {}), + ...(components?.length ? { components } : {}), files: [ { data: media.buffer, @@ -329,6 +334,7 @@ async function sendDiscordMedia( request, maxLinesPerMessage, undefined, + undefined, chunkMode, ); }