From 122bdfa4e1e8a1ce8062487002c6ad16de8f31d5 Mon Sep 17 00:00:00 2001 From: Wei He Date: Sat, 14 Feb 2026 10:33:36 -0500 Subject: [PATCH] feat(discord): add configurable ephemeral option for slash commands --- docs/channels/discord.md | 6 +++++- src/config/types.discord.ts | 7 +++++++ src/config/zod-schema.providers-core.ts | 6 ++++++ src/discord/monitor/commands.test.ts | 24 ++++++++++++++++++++++++ src/discord/monitor/commands.ts | 9 +++++++++ src/discord/monitor/provider.ts | 4 +++- 6 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/discord/monitor/commands.test.ts create mode 100644 src/discord/monitor/commands.ts diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 0970a88c7..044e87840 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -534,6 +534,10 @@ Use `bindings[].match.roles` to route Discord guild members to different agents See [Slash commands](/tools/slash-commands) for command catalog and behavior. +Default slash command settings: + +- `ephemeral: true` + ## Feature details @@ -969,7 +973,7 @@ High-signal Discord fields: - startup/auth: `enabled`, `token`, `accounts.*`, `allowBots` - policy: `groupPolicy`, `dm.*`, `guilds.*`, `guilds.*.channels.*` -- command: `commands.native`, `commands.useAccessGroups`, `configWrites` +- command: `commands.native`, `commands.useAccessGroups`, `configWrites`, `slashCommand.*` - reply/history: `replyToMode`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` - delivery: `textChunkLimit`, `chunkMode`, `maxLinesPerMessage` - streaming: `streamMode`, `draftChunk`, `blockStreaming`, `blockStreamingCoalesce` diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index 95ce774da..7e4701201 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -142,6 +142,11 @@ export type DiscordUiConfig = { components?: DiscordUiComponentsConfig; }; +export type DiscordSlashCommandConfig = { + /** Reply ephemerally (default: true). */ + ephemeral?: boolean; +}; + export type DiscordAccountConfig = { /** Optional display name for this account (used in CLI/UI lists). */ name?: string; @@ -226,6 +231,8 @@ export type DiscordAccountConfig = { agentComponents?: DiscordAgentComponentsConfig; /** Discord UI customization (components, modals, etc.). */ ui?: DiscordUiConfig; + /** Slash command configuration. */ + slashCommand?: DiscordSlashCommandConfig; /** Privileged Gateway Intents (must also be enabled in Discord Developer Portal). */ intents?: DiscordIntentsConfig; /** Voice channel conversation settings. */ diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 0ac13ae8d..5109ac269 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -357,6 +357,12 @@ export const DiscordAccountSchema = z .strict() .optional(), ui: DiscordUiSchema, + slashCommand: z + .object({ + ephemeral: z.boolean().optional(), + }) + .strict() + .optional(), intents: z .object({ presence: z.boolean().optional(), diff --git a/src/discord/monitor/commands.test.ts b/src/discord/monitor/commands.test.ts new file mode 100644 index 000000000..c50bf17a7 --- /dev/null +++ b/src/discord/monitor/commands.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { resolveDiscordSlashCommandConfig } from "./commands.js"; + +describe("resolveDiscordSlashCommandConfig", () => { + it("defaults ephemeral to true when undefined", () => { + const result = resolveDiscordSlashCommandConfig(undefined); + expect(result.ephemeral).toBe(true); + }); + + it("defaults ephemeral to true when not explicitly false", () => { + const result = resolveDiscordSlashCommandConfig({}); + expect(result.ephemeral).toBe(true); + }); + + it("sets ephemeral to false when explicitly false", () => { + const result = resolveDiscordSlashCommandConfig({ ephemeral: false }); + expect(result.ephemeral).toBe(false); + }); + + it("keeps ephemeral true when explicitly true", () => { + const result = resolveDiscordSlashCommandConfig({ ephemeral: true }); + expect(result.ephemeral).toBe(true); + }); +}); diff --git a/src/discord/monitor/commands.ts b/src/discord/monitor/commands.ts new file mode 100644 index 000000000..96a277785 --- /dev/null +++ b/src/discord/monitor/commands.ts @@ -0,0 +1,9 @@ +import type { DiscordSlashCommandConfig } from "../../config/types.discord.js"; + +export function resolveDiscordSlashCommandConfig( + raw?: DiscordSlashCommandConfig, +): Required { + return { + ephemeral: raw?.ephemeral !== false, + }; +} diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index 21c552ec0..490590cc5 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -54,6 +54,7 @@ import { createDiscordComponentStringSelect, createDiscordComponentUserSelect, } from "./agent-components.js"; +import { resolveDiscordSlashCommandConfig } from "./commands.js"; import { createExecApprovalButton, DiscordExecApprovalHandler } from "./exec-approvals.js"; import { createDiscordGatewayPlugin } from "./gateway-plugin.js"; import { registerGateway, unregisterGateway } from "./gateway-registry.js"; @@ -246,8 +247,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { globalSetting: cfg.commands?.native, }); const useAccessGroups = cfg.commands?.useAccessGroups !== false; + const slashCommand = resolveDiscordSlashCommandConfig(discordCfg.slashCommand); const sessionPrefix = "discord:slash"; - const ephemeralDefault = true; + const ephemeralDefault = slashCommand.ephemeral; const voiceEnabled = discordCfg.voice?.enabled !== false; if (token) {