From fb32a4c3cdc168c978d82792deb01929cb133e96 Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 11 Mar 2026 16:54:19 -0700 Subject: [PATCH] fix(discord): route thread messages with per-thread conversations (#564) --- src/channels/discord.ts | 17 ++++++++++------- src/core/types.ts | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/channels/discord.ts b/src/channels/discord.ts index bd31d46..ae34978 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -318,7 +318,7 @@ Ask the bot owner to approve with: let commandForcePerChat = false; if (isGroup && this.config.groups) { const threadMode = resolveDiscordThreadMode(this.config.groups, keys); - commandForcePerChat = threadMode === 'thread-only'; + commandForcePerChat = threadMode === 'thread-only' || isThreadMessage; if (commandForcePerChat && !isThreadMessage) { const shouldCreateThread = wasMentioned && resolveDiscordAutoCreateThreadOnMention(this.config.groups, keys); @@ -464,7 +464,8 @@ Ask the bot owner to approve with: serverId: message.guildId || undefined, wasMentioned, isListeningMode, - forcePerChat: isThreadOnly || undefined, + threadId: isThreadMessage ? effectiveChatId : undefined, + forcePerChat: (isThreadOnly || isThreadMessage) || undefined, attachments, formatterHints: this.getFormatterHints(), }); @@ -499,9 +500,10 @@ Ask the bot owner to approve with: async sendMessage(msg: OutboundMessage): Promise<{ messageId: string }> { if (!this.client) throw new Error('Discord not started'); - const channel = await this.client.channels.fetch(msg.chatId); + const targetChannelId = msg.threadId || msg.chatId; + const channel = await this.client.channels.fetch(targetChannelId); if (!channel || !channel.isTextBased() || !('send' in channel)) { - throw new Error(`Discord channel not found or not text-based: ${msg.chatId}`); + throw new Error(`Discord channel not found or not text-based: ${targetChannelId}`); } const sendable = channel as { send: (content: string) => Promise<{ id: string }> }; @@ -516,9 +518,10 @@ Ask the bot owner to approve with: async sendFile(file: OutboundFile): Promise<{ messageId: string }> { if (!this.client) throw new Error('Discord not started'); - const channel = await this.client.channels.fetch(file.chatId); + const targetChannelId = file.threadId || file.chatId; + const channel = await this.client.channels.fetch(targetChannelId); if (!channel || !channel.isTextBased() || !('send' in channel)) { - throw new Error(`Discord channel not found or not text-based: ${file.chatId}`); + throw new Error(`Discord channel not found or not text-based: ${targetChannelId}`); } const payload = { @@ -697,7 +700,7 @@ Ask the bot owner to approve with: groupName, serverId: message.guildId || undefined, isListeningMode, - forcePerChat: reactionForcePerChat || undefined, + forcePerChat: (reactionForcePerChat || isThreadMessage) || undefined, reaction: { emoji, messageId: message.id, diff --git a/src/core/types.ts b/src/core/types.ts index 174c22d..ae11c05 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -125,7 +125,7 @@ export interface OutboundMessage { chatId: string; text: string; replyToMessageId?: string; - threadId?: string; // Slack thread_ts + threadId?: string; // Thread ID (Slack thread_ts / Discord thread channel) /** When set, tells the adapter which parse mode to use (e.g., 'MarkdownV2', * 'HTML') and to skip its default markdown conversion. Adapters that don't * support the specified mode ignore this and fall back to default. */