Skip to content

Commit b09a595

Browse files
fix(discord): support applied_tags parameter for forum thread creation
Forum channels that require tags fail with "A tag is required" when creating threads because there was no way to pass tag IDs. Add appliedTags parameter to the thread-create action so forum posts can include required tags from the channel's available_tags list.
1 parent 0f72000 commit b09a595

File tree

6 files changed

+56
-6
lines changed

6 files changed

+56
-6
lines changed

src/agents/tools/discord-actions-messaging.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -363,13 +363,17 @@ export async function handleDiscordMessagingAction(
363363
typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw)
364364
? autoArchiveMinutesRaw
365365
: undefined;
366+
const appliedTags = readStringArrayParam(params, "appliedTags");
367+
const payload = {
368+
name,
369+
messageId,
370+
autoArchiveMinutes,
371+
content,
372+
appliedTags: appliedTags ?? undefined,
373+
};
366374
const thread = accountId
367-
? await createThreadDiscord(
368-
channelId,
369-
{ name, messageId, autoArchiveMinutes, content },
370-
{ accountId },
371-
)
372-
: await createThreadDiscord(channelId, { name, messageId, autoArchiveMinutes, content });
375+
? await createThreadDiscord(channelId, payload, { accountId })
376+
: await createThreadDiscord(channelId, payload);
373377
return jsonResult({ ok: true, thread });
374378
}
375379
case "threadList": {

src/agents/tools/message-tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ function buildThreadSchema() {
311311
return {
312312
threadName: Type.Optional(Type.String()),
313313
autoArchiveMin: Type.Optional(Type.Number()),
314+
appliedTags: Type.Optional(Type.Array(Type.String())),
314315
};
315316
}
316317

src/channels/plugins/actions/discord/handle-action.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ export async function handleDiscordMessageAction(
230230
const autoArchiveMinutes = readNumberParam(params, "autoArchiveMin", {
231231
integer: true,
232232
});
233+
const appliedTags = readStringArrayParam(params, "appliedTags");
233234
return await handleDiscordAction(
234235
{
235236
action: "threadCreate",
@@ -239,6 +240,7 @@ export async function handleDiscordMessageAction(
239240
messageId,
240241
content,
241242
autoArchiveMinutes,
243+
appliedTags: appliedTags ?? undefined,
242244
},
243245
cfg,
244246
actionOptions,

src/discord/send.creates-thread.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,44 @@ describe("sendMessageDiscord", () => {
7676
);
7777
});
7878

79+
it("passes applied_tags for forum threads", async () => {
80+
const { rest, getMock, postMock } = makeDiscordRest();
81+
getMock.mockResolvedValue({ type: ChannelType.GuildForum });
82+
postMock.mockResolvedValue({ id: "t1" });
83+
await createThreadDiscord(
84+
"chan1",
85+
{ name: "tagged post", appliedTags: ["tag1", "tag2"] },
86+
{ rest, token: "t" },
87+
);
88+
expect(postMock).toHaveBeenCalledWith(
89+
Routes.threads("chan1"),
90+
expect.objectContaining({
91+
body: {
92+
name: "tagged post",
93+
message: { content: "tagged post" },
94+
applied_tags: ["tag1", "tag2"],
95+
},
96+
}),
97+
);
98+
});
99+
100+
it("omits applied_tags for non-forum threads", async () => {
101+
const { rest, getMock, postMock } = makeDiscordRest();
102+
getMock.mockResolvedValue({ type: ChannelType.GuildText });
103+
postMock.mockResolvedValue({ id: "t1" });
104+
await createThreadDiscord(
105+
"chan1",
106+
{ name: "thread", appliedTags: ["tag1"] },
107+
{ rest, token: "t" },
108+
);
109+
expect(postMock).toHaveBeenCalledWith(
110+
Routes.threads("chan1"),
111+
expect.objectContaining({
112+
body: expect.not.objectContaining({ applied_tags: expect.anything() }),
113+
}),
114+
);
115+
});
116+
79117
it("falls back when channel lookup is unavailable", async () => {
80118
const { rest, getMock, postMock } = makeDiscordRest();
81119
getMock.mockRejectedValue(new Error("lookup failed"));

src/discord/send.messages.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ export async function createThreadDiscord(
124124
if (isForumLike) {
125125
const starterContent = payload.content?.trim() ? payload.content : payload.name;
126126
body.message = { content: starterContent };
127+
if (payload.appliedTags?.length) {
128+
body.applied_tags = payload.appliedTags;
129+
}
127130
}
128131
// When creating a standalone thread (no messageId) in a non-forum channel,
129132
// default to public thread (type 11). Discord defaults to private (type 12)

src/discord/send.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export type DiscordThreadCreate = {
7474
content?: string;
7575
/** Discord thread type (default: PublicThread for standalone threads). */
7676
type?: number;
77+
/** Tag IDs to apply when creating a forum/media thread (Discord `applied_tags`). */
78+
appliedTags?: string[];
7779
};
7880

7981
export type DiscordThreadList = {

0 commit comments

Comments
 (0)