Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions apps/api/src/handlers/discord/__tests__/task-launch.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions apps/api/src/handlers/discord/task-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { db, environments, eq, sql, taskRuns, tasks } from '@roomote/db/server';
import {
enqueueTask,
getTaskUrl,
selectDiscordForumTag,
type RoutingWorkspace,
} from '@roomote/cloud-agents/server';
import {
Expand Down Expand Up @@ -385,6 +386,14 @@ export async function launchDiscordTask(input: {
channelId: parentId,
name: buildCommunicationTaskThreadName(input.queuedMessage.text),
initialText,
selectForumTag: async (availableTags) =>
(
await selectDiscordForumTag({
taskDescription: input.queuedMessage.text,
availableTags,
tracking: { userId: input.launchOwnerUserId },
})
)?.tagId ?? null,
});
// Persist the external coordinate before the public-thread starter is
// sent, so a failed send can resume in this exact thread on redelivery.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions apps/api/src/handlers/tasks/automation-work-items/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type DiscordCommunicationProvider,
type DiscordTaskThread,
} from '@roomote/communication/discord-provider';
import { selectDiscordForumTag } from '@roomote/cloud-agents/server';
import { getRedis } from '@roomote/redis';
import {
findDiscordAutomationDestination,
Expand Down Expand Up @@ -172,6 +173,16 @@ export async function createAutomationDiscordTaskThread(params: {
channelId: params.target.channelId,
name: buildCommunicationTaskThreadName(params.workItem.title),
initialText,
selectForumTag: async (availableTags) =>
(
await selectDiscordForumTag({
taskDescription: [
params.workItem.title,
params.workItem.brief,
].join('\n\n'),
availableTags,
})
)?.tagId ?? null,
});
// A queue publish failure reopens this same work item. Save the Discord
// coordinate before posting the starter so that every retry resumes
Expand Down
9 changes: 7 additions & 2 deletions apps/docs/providers/communications/discord.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,15 @@ For tasks started in a server, the bot needs these permissions:
- create public threads
- send messages in threads
- add reactions
- manage threads (only when a required-tag forum has no unmoderated tags)

A task started from a normal text or announcement channel runs in a public
thread started on the message that requested it — like a threaded reply — so
the conversation stays anchored where it began. A task started in a forum gets
its own forum post, and `/new` starts a fresh, separate task thread.
its own forum post, and `/new` starts a fresh, separate task thread. When a
forum requires a tag, Roomote applies an available unmoderated tag
automatically. If every tag is moderated, the bot needs Manage Threads to
apply one.

<Warning>
Unlike a Slack workspace, a Discord server can be public or shared with
Expand Down Expand Up @@ -92,7 +96,8 @@ messages that are not replies to an existing conversation) to a default
channel you pick. Choose it in the Discord section of Roomote's comms settings:
select a server and a text or forum channel where the bot has permission to
post and create threads. Replies always stay in the conversation they started
in. Forum channels that require a tag are not supported as the default channel.
in. Required-tag forums work when Roomote can apply a usable tag — an
unmoderated tag, or a moderated tag when the bot has Manage Threads.

Individual automations can also report to their own Discord channel instead of
the default: pick a Discord channel in that automation's destination selector
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,10 @@ export function ProviderSetupInstructions({
<InstructionText heading="Installation permissions">
Roomote needs View Channels, Send Messages, Read Message History,
Embed Links, Attach Files, Create Public Threads, Send Messages in
Threads, and Add Reactions. After you save the bot token, the Add to
Discord button appears and requests these permissions automatically.
Threads, Add Reactions, and Manage Threads. Manage Threads is only
required for required-tag forums that expose only moderated tags.
After you save the bot token, the Add to Discord button appears and
requests these permissions automatically.
</InstructionText>
<InstructionText heading="Bot token">
Paste the token below. Roomote derives the bot and application names
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export function DiscordDefaultChannelPicker() {
>
#{channel.name}
{channel.kind === 'forum' ? ' (forum)' : ''}
{channel.supported ? '' : ' — requires a tag'}
{channel.supported ? '' : ' — no available tag'}
</SelectItem>
))}
</SelectContent>
Expand Down
17 changes: 16 additions & 1 deletion apps/web/src/lib/discord-install.ts
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
export const DISCORD_INSTALL_PERMISSIONS = '309237763136';
// Bitfield requested by the Add to Discord install link:
// View Channels, Send Messages, Read Message History, Embed Links, Attach
// Files, Add Reactions, Manage Threads, Create Public Threads, and Send
// Messages in Threads. Manage Threads is needed so Roomote can apply a
// moderated tag when a required-tag forum has no unmoderated tags.
export const DISCORD_INSTALL_PERMISSIONS = String(
(1n << 6n) | // add_reactions
(1n << 10n) | // view_channel
(1n << 11n) | // send_messages
(1n << 14n) | // embed_links
(1n << 15n) | // attach_files
(1n << 16n) | // read_message_history
(1n << 34n) | // manage_threads
(1n << 35n) | // create_public_threads
(1n << 38n), // send_messages_in_threads
);
10 changes: 5 additions & 5 deletions apps/web/src/trpc/commands/comms/index.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions apps/web/src/trpc/commands/comms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,8 @@ export async function listDiscordChannelsCommand(
return {
channels: channels.map((channel) => {
const requiresTag = discordChannelRequiresTag(channel);
const requiredTagUnavailable =
requiresTag && (channel.availableTags?.length ?? 0) === 0;
return {
id: channel.id,
name: channel.name,
Expand All @@ -743,7 +745,7 @@ export async function listDiscordChannelsCommand(
flags: channel.flags ?? 0,
availableTags: channel.availableTags ?? [],
requiresTag,
supported: !requiresTag,
supported: !requiredTagUnavailable,
};
}),
};
Expand Down Expand Up @@ -771,8 +773,16 @@ export async function selectDiscordDestinationCommand(
}
const permissions = await diagnoseDiscordPermissionsCommand(auth, input);
if (!permissions.canUseChannel) {
const missing = permissions.missingPermissions
.map((name) =>
name
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' '),
)
.join(', ');
throw new Error(
`Roomote is missing required permissions in #${channel.name}: ${permissions.missingPermissions.join(', ')}.`,
`Roomote is missing required permissions in #${channel.name}: ${missing}.`,
);
}
return captureDiscordDefaultDestination({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const NON_TASK_INFERENCE_SURFACES = {
fastAgentQuestionAnswering: 'fast_agent_question_answering',
prReviewNotificationTriage: 'pr_review_notification_triage',
routerChannelLaunchGate: 'router_channel_launch_gate',
routerDiscordForumTag: 'router_discord_forum_tag',
routerFollowupClassification: 'router_followup_classification',
routerGitHubRouting: 'router_github_routing',
routerTaskRouting: 'router_task_routing',
Expand Down
Loading
Loading