Commands: add dynamic arg menus

This commit is contained in:
Shadow
2026-01-15 01:13:36 -06:00
committed by Peter Steinberger
parent 7e1e7ba2d8
commit 74bc5bfd7c
10 changed files with 1262 additions and 241 deletions

View File

@@ -1,9 +1,34 @@
import { ChannelType, Command, type CommandInteraction, type CommandOptions } from "@buape/carbon";
import { ApplicationCommandOptionType } from "discord-api-types/v10";
import {
Button,
ChannelType,
Command,
Row,
type AutocompleteInteraction,
type ButtonInteraction,
type CommandInteraction,
type CommandOptions,
type ComponentData,
} from "@buape/carbon";
import { ApplicationCommandOptionType, ButtonStyle } from "discord-api-types/v10";
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
import { buildCommandText } from "../../auto-reply/commands-registry.js";
import {
buildCommandTextFromArgs,
findCommandByNativeName,
listChatCommands,
parseCommandArgs,
resolveCommandArgChoices,
resolveCommandArgMenu,
serializeCommandArgs,
} from "../../auto-reply/commands-registry.js";
import type {
ChatCommandDefinition,
CommandArgDefinition,
CommandArgValues,
CommandArgs,
NativeCommandSpec,
} from "../../auto-reply/commands-registry.js";
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
import type { ReplyPayload } from "../../auto-reply/types.js";
import type { ClawdbotConfig, loadConfig } from "../../config/config.js";
@@ -28,12 +53,261 @@ import { formatDiscordUserTag } from "./format.js";
type DiscordConfig = NonNullable<ClawdbotConfig["channels"]>["discord"];
export function createDiscordNativeCommand(params: {
command: {
name: string;
description: string;
acceptsArgs: boolean;
function buildDiscordCommandOptions(params: {
command: ChatCommandDefinition;
cfg: ReturnType<typeof loadConfig>;
}): CommandOptions | undefined {
const { command, cfg } = params;
const args = command.args;
if (!args || args.length === 0) return undefined;
return args.map((arg) => {
const required = arg.required ?? false;
if (arg.type === "number") {
return {
name: arg.name,
description: arg.description,
type: ApplicationCommandOptionType.Number,
required,
};
}
if (arg.type === "boolean") {
return {
name: arg.name,
description: arg.description,
type: ApplicationCommandOptionType.Boolean,
required,
};
}
const resolvedChoices = resolveCommandArgChoices({ command, arg, cfg });
const shouldAutocomplete =
resolvedChoices.length > 0 &&
(typeof arg.choices === "function" || resolvedChoices.length > 25);
const autocomplete = shouldAutocomplete
? async (interaction: AutocompleteInteraction) => {
const focused = interaction.options.getFocused();
const focusValue =
typeof focused?.value === "string" ? focused.value.trim().toLowerCase() : "";
const choices = resolveCommandArgChoices({ command, arg, cfg });
const filtered = focusValue
? choices.filter((choice) => choice.toLowerCase().includes(focusValue))
: choices;
await interaction.respond(
filtered.slice(0, 25).map((choice) => ({ name: choice, value: choice })),
);
}
: undefined;
const choices =
resolvedChoices.length > 0 && !autocomplete
? resolvedChoices.slice(0, 25).map((choice) => ({ name: choice, value: choice }))
: undefined;
return {
name: arg.name,
description: arg.description,
type: ApplicationCommandOptionType.String,
required,
choices,
autocomplete,
};
}) satisfies CommandOptions;
}
function readDiscordCommandArgs(
interaction: CommandInteraction,
definitions?: CommandArgDefinition[],
): CommandArgs | undefined {
if (!definitions || definitions.length === 0) return undefined;
const values: CommandArgValues = {};
for (const definition of definitions) {
let value: unknown;
if (definition.type === "number") {
value = interaction.options.getNumber(definition.name);
} else if (definition.type === "boolean") {
value = interaction.options.getBoolean(definition.name);
} else {
value = interaction.options.getString(definition.name);
}
if (value != null) {
values[definition.name] = value;
}
}
return Object.keys(values).length > 0 ? { values } : undefined;
}
function chunkItems<T>(items: T[], size: number): T[][] {
if (size <= 0) return [items];
const rows: T[][] = [];
for (let i = 0; i < items.length; i += size) {
rows.push(items.slice(i, i + size));
}
return rows;
}
const DISCORD_COMMAND_ARG_CUSTOM_ID_KEY = "cmdarg";
function createCommandArgsWithValue(params: { argName: string; value: string }): CommandArgs {
const values: CommandArgValues = { [params.argName]: params.value };
return { values };
}
function encodeDiscordCommandArgValue(value: string): string {
return encodeURIComponent(value);
}
function decodeDiscordCommandArgValue(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function buildDiscordCommandArgCustomId(params: {
command: string;
arg: string;
value: string;
userId: string;
}): string {
return [
`${DISCORD_COMMAND_ARG_CUSTOM_ID_KEY}:command=${encodeDiscordCommandArgValue(params.command)}`,
`arg=${encodeDiscordCommandArgValue(params.arg)}`,
`value=${encodeDiscordCommandArgValue(params.value)}`,
`user=${encodeDiscordCommandArgValue(params.userId)}`,
].join(";");
}
function parseDiscordCommandArgData(
data: ComponentData,
): { command: string; arg: string; value: string; userId: string } | null {
if (!data || typeof data !== "object") return null;
const coerce = (value: unknown) =>
typeof value === "string" || typeof value === "number" ? String(value) : "";
const rawCommand = coerce(data.command);
const rawArg = coerce(data.arg);
const rawValue = coerce(data.value);
const rawUser = coerce(data.user);
if (!rawCommand || !rawArg || !rawValue || !rawUser) return null;
return {
command: decodeDiscordCommandArgValue(rawCommand),
arg: decodeDiscordCommandArgValue(rawArg),
value: decodeDiscordCommandArgValue(rawValue),
userId: decodeDiscordCommandArgValue(rawUser),
};
}
class DiscordCommandArgButton extends Button {
label: string;
customId: string;
style = ButtonStyle.Secondary;
private cfg: ReturnType<typeof loadConfig>;
private discordConfig: DiscordConfig;
private accountId: string;
private sessionPrefix: string;
constructor(params: {
label: string;
customId: string;
cfg: ReturnType<typeof loadConfig>;
discordConfig: DiscordConfig;
accountId: string;
sessionPrefix: string;
}) {
super();
this.label = params.label;
this.customId = params.customId;
this.cfg = params.cfg;
this.discordConfig = params.discordConfig;
this.accountId = params.accountId;
this.sessionPrefix = params.sessionPrefix;
}
async run(interaction: ButtonInteraction, data: ComponentData) {
const parsed = parseDiscordCommandArgData(data);
if (!parsed) {
await interaction.update({
content: "Sorry, that selection is no longer available.",
components: [],
});
return;
}
if (interaction.user?.id && interaction.user.id !== parsed.userId) {
await interaction.acknowledge();
return;
}
const commandDefinition =
findCommandByNativeName(parsed.command) ??
listChatCommands().find((entry) => entry.key === parsed.command);
if (!commandDefinition) {
await interaction.update({
content: "Sorry, that command is no longer available.",
components: [],
});
return;
}
await interaction.update({
content: `✅ Selected ${parsed.value}.`,
components: [],
});
const commandArgs = createCommandArgsWithValue({
argName: parsed.arg,
value: parsed.value,
});
const commandArgsWithRaw: CommandArgs = {
...commandArgs,
raw: serializeCommandArgs(commandDefinition, commandArgs),
};
const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw);
await dispatchDiscordCommandInteraction({
interaction,
prompt,
command: commandDefinition,
commandArgs: commandArgsWithRaw,
cfg: this.cfg,
discordConfig: this.discordConfig,
accountId: this.accountId,
sessionPrefix: this.sessionPrefix,
preferFollowUp: true,
});
}
}
function buildDiscordCommandArgMenu(params: {
command: ChatCommandDefinition;
menu: { arg: CommandArgDefinition; choices: string[]; title?: string };
interaction: CommandInteraction;
cfg: ReturnType<typeof loadConfig>;
discordConfig: DiscordConfig;
accountId: string;
sessionPrefix: string;
}): { content: string; components: Row<Button>[] } {
const { command, menu, interaction } = params;
const commandLabel = command.nativeName ?? command.key;
const userId = interaction.user?.id ?? "";
const rows = chunkItems(menu.choices, 4).map((choices) => {
const buttons = choices.map(
(choice) =>
new DiscordCommandArgButton({
label: choice,
customId: buildDiscordCommandArgCustomId({
command: commandLabel,
arg: menu.arg.name,
value: choice,
userId,
}),
cfg: params.cfg,
discordConfig: params.discordConfig,
accountId: params.accountId,
sessionPrefix: params.sessionPrefix,
}),
);
return new Row(buttons);
});
const content =
menu.title ?? `Choose ${menu.arg.description || menu.arg.name} for /${commandLabel}.`;
return { content, components: rows };
}
export function createDiscordNativeCommand(params: {
command: NativeCommandSpec;
cfg: ReturnType<typeof loadConfig>;
discordConfig: DiscordConfig;
accountId: string;
@@ -41,12 +315,26 @@ export function createDiscordNativeCommand(params: {
ephemeralDefault: boolean;
}) {
const { command, cfg, discordConfig, accountId, sessionPrefix, ephemeralDefault } = params;
return new (class extends Command {
name = command.name;
description = command.description;
defer = true;
ephemeral = ephemeralDefault;
options = command.acceptsArgs
const commandDefinition =
findCommandByNativeName(command.name) ??
({
key: command.name,
nativeName: command.name,
description: command.description,
textAliases: [],
acceptsArgs: command.acceptsArgs,
args: command.args,
argsParsing: "none",
scope: "native",
} satisfies ChatCommandDefinition);
const argDefinitions = commandDefinition.args ?? command.args;
const commandOptions = buildDiscordCommandOptions({
command: commandDefinition,
cfg,
});
const options = commandOptions
? (commandOptions satisfies CommandOptions)
: command.acceptsArgs
? ([
{
name: "input",
@@ -56,219 +344,301 @@ export function createDiscordNativeCommand(params: {
},
] satisfies CommandOptions)
: undefined;
return new (class extends Command {
name = command.name;
description = command.description;
defer = true;
ephemeral = ephemeralDefault;
options = options;
async run(interaction: CommandInteraction) {
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const user = interaction.user;
if (!user) return;
const channel = interaction.channel;
const channelType = channel?.type;
const isDirectMessage = channelType === ChannelType.DM;
const isGroupDm = channelType === ChannelType.GroupDM;
const channelName = channel && "name" in channel ? (channel.name as string) : undefined;
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
const prompt = buildCommandText(
this.name,
command.acceptsArgs ? interaction.options.getString("input") : undefined,
);
const guildInfo = resolveDiscordGuildEntry({
guild: interaction.guild ?? undefined,
guildEntries: discordConfig?.guilds,
});
const channelConfig = interaction.guild
? resolveDiscordChannelConfig({
guildInfo,
channelId: channel?.id ?? "",
channelName,
channelSlug,
})
: null;
if (channelConfig?.enabled === false) {
await interaction.reply({
content: "This channel is disabled.",
});
return;
}
if (interaction.guild && channelConfig?.allowed === false) {
await interaction.reply({
content: "This channel is not allowed.",
});
return;
}
if (useAccessGroups && interaction.guild) {
const channelAllowlistConfigured =
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
const channelAllowed = channelConfig?.allowed !== false;
const allowByPolicy = isDiscordGroupAllowedByPolicy({
groupPolicy: discordConfig?.groupPolicy ?? "open",
channelAllowlistConfigured,
channelAllowed,
});
if (!allowByPolicy) {
await interaction.reply({
content: "This channel is not allowed.",
});
return;
}
}
const dmEnabled = discordConfig?.dm?.enabled ?? true;
const dmPolicy = discordConfig?.dm?.policy ?? "pairing";
let commandAuthorized = true;
if (isDirectMessage) {
if (!dmEnabled || dmPolicy === "disabled") {
await interaction.reply({ content: "Discord DMs are disabled." });
return;
}
if (dmPolicy !== "open") {
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
const effectiveAllowFrom = [...(discordConfig?.dm?.allowFrom ?? []), ...storeAllowFrom];
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]);
const permitted = allowList
? allowListMatches(allowList, {
id: user.id,
name: user.username,
tag: formatDiscordUserTag(user),
})
: false;
if (!permitted) {
commandAuthorized = false;
if (dmPolicy === "pairing") {
const { code, created } = await upsertChannelPairingRequest({
channel: "discord",
id: user.id,
meta: {
tag: formatDiscordUserTag(user),
name: user.username ?? undefined,
},
});
if (created) {
await interaction.reply({
content: buildPairingReply({
channel: "discord",
idLine: `Your Discord user id: ${user.id}`,
code,
}),
ephemeral: true,
});
}
} else {
await interaction.reply({
content: "You are not authorized to use this command.",
ephemeral: true,
});
}
return;
}
commandAuthorized = true;
}
}
if (!isDirectMessage) {
const channelUsers = channelConfig?.users ?? guildInfo?.users;
if (Array.isArray(channelUsers) && channelUsers.length > 0) {
const userOk = resolveDiscordUserAllowed({
allowList: channelUsers,
userId: user.id,
userName: user.username,
userTag: formatDiscordUserTag(user),
});
if (!userOk) {
await interaction.reply({
content: "You are not authorized to use this command.",
});
return;
}
}
}
if (isGroupDm && discordConfig?.dm?.groupEnabled === false) {
await interaction.reply({ content: "Discord group DMs are disabled." });
return;
}
const isGuild = Boolean(interaction.guild);
const channelId = channel?.id ?? "unknown";
const interactionId = interaction.rawData.id;
const route = resolveAgentRoute({
const commandArgs = argDefinitions?.length
? readDiscordCommandArgs(interaction, argDefinitions)
: command.acceptsArgs
? parseCommandArgs(commandDefinition, interaction.options.getString("input") ?? "")
: undefined;
const commandArgsWithRaw = commandArgs
? ({
...commandArgs,
raw: serializeCommandArgs(commandDefinition, commandArgs) ?? commandArgs.raw,
} satisfies CommandArgs)
: undefined;
const prompt = buildCommandTextFromArgs(commandDefinition, commandArgsWithRaw);
await dispatchDiscordCommandInteraction({
interaction,
prompt,
command: commandDefinition,
commandArgs: commandArgsWithRaw,
cfg,
channel: "discord",
discordConfig,
accountId,
guildId: interaction.guild?.id ?? undefined,
peer: {
kind: isDirectMessage ? "dm" : isGroupDm ? "group" : "channel",
id: isDirectMessage ? user.id : channelId,
},
});
const ctxPayload = {
Body: prompt,
CommandBody: prompt,
From: isDirectMessage ? `discord:${user.id}` : `group:${channelId}`,
To: `slash:${user.id}`,
SessionKey: `agent:${route.agentId}:${sessionPrefix}:${user.id}`,
CommandTargetSessionKey: route.sessionKey,
AccountId: route.accountId,
ChatType: isDirectMessage ? "direct" : "group",
GroupSubject: isGuild ? interaction.guild?.name : undefined,
GroupSystemPrompt: isGuild
? (() => {
const channelTopic =
channel && "topic" in channel ? (channel.topic ?? undefined) : undefined;
const channelDescription = channelTopic?.trim();
const systemPromptParts = [
channelDescription ? `Channel topic: ${channelDescription}` : null,
channelConfig?.systemPrompt?.trim() || null,
].filter((entry): entry is string => Boolean(entry));
return systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
})()
: undefined,
SenderName: user.globalName ?? user.username,
SenderId: user.id,
SenderUsername: user.username,
SenderTag: formatDiscordUserTag(user),
Provider: "discord" as const,
Surface: "discord" as const,
WasMentioned: true,
MessageSid: interactionId,
Timestamp: Date.now(),
CommandAuthorized: commandAuthorized,
CommandSource: "native" as const,
};
let didReply = false;
await dispatchReplyWithDispatcher({
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => {
await deliverDiscordInteractionReply({
interaction,
payload,
textLimit: resolveTextChunkLimit(cfg, "discord", accountId, {
fallbackLimit: 2000,
}),
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
preferFollowUp: didReply,
});
didReply = true;
},
onError: (err, info) => {
console.error(`discord slash ${info.kind} reply failed`, err);
},
},
replyOptions: {
skillFilter: channelConfig?.skills,
disableBlockStreaming:
typeof discordConfig?.blockStreaming === "boolean"
? !discordConfig.blockStreaming
: undefined,
},
sessionPrefix,
preferFollowUp: false,
});
}
})();
}
async function dispatchDiscordCommandInteraction(params: {
interaction: CommandInteraction | ButtonInteraction;
prompt: string;
command: ChatCommandDefinition;
commandArgs?: CommandArgs;
cfg: ReturnType<typeof loadConfig>;
discordConfig: DiscordConfig;
accountId: string;
sessionPrefix: string;
preferFollowUp: boolean;
}) {
const {
interaction,
prompt,
command,
commandArgs,
cfg,
discordConfig,
accountId,
sessionPrefix,
preferFollowUp,
} = params;
const respond = async (content: string, options?: { ephemeral?: boolean }) => {
const payload = {
content,
...(options?.ephemeral !== undefined ? { ephemeral: options.ephemeral } : {}),
};
if (preferFollowUp) {
await interaction.followUp(payload);
return;
}
await interaction.reply(payload);
};
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const user = interaction.user;
if (!user) return;
const channel = interaction.channel;
const channelType = channel?.type;
const isDirectMessage = channelType === ChannelType.DM;
const isGroupDm = channelType === ChannelType.GroupDM;
const channelName = channel && "name" in channel ? (channel.name as string) : undefined;
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
const guildInfo = resolveDiscordGuildEntry({
guild: interaction.guild ?? undefined,
guildEntries: discordConfig?.guilds,
});
const channelConfig = interaction.guild
? resolveDiscordChannelConfig({
guildInfo,
channelId: channel?.id ?? "",
channelName,
channelSlug,
})
: null;
if (channelConfig?.enabled === false) {
await respond("This channel is disabled.");
return;
}
if (interaction.guild && channelConfig?.allowed === false) {
await respond("This channel is not allowed.");
return;
}
if (useAccessGroups && interaction.guild) {
const channelAllowlistConfigured =
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
const channelAllowed = channelConfig?.allowed !== false;
const allowByPolicy = isDiscordGroupAllowedByPolicy({
groupPolicy: discordConfig?.groupPolicy ?? "open",
channelAllowlistConfigured,
channelAllowed,
});
if (!allowByPolicy) {
await respond("This channel is not allowed.");
return;
}
}
const dmEnabled = discordConfig?.dm?.enabled ?? true;
const dmPolicy = discordConfig?.dm?.policy ?? "pairing";
let commandAuthorized = true;
if (isDirectMessage) {
if (!dmEnabled || dmPolicy === "disabled") {
await respond("Discord DMs are disabled.");
return;
}
if (dmPolicy !== "open") {
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
const effectiveAllowFrom = [...(discordConfig?.dm?.allowFrom ?? []), ...storeAllowFrom];
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]);
const permitted = allowList
? allowListMatches(allowList, {
id: user.id,
name: user.username,
tag: formatDiscordUserTag(user),
})
: false;
if (!permitted) {
commandAuthorized = false;
if (dmPolicy === "pairing") {
const { code, created } = await upsertChannelPairingRequest({
channel: "discord",
id: user.id,
meta: {
tag: formatDiscordUserTag(user),
name: user.username ?? undefined,
},
});
if (created) {
await respond(
buildPairingReply({
channel: "discord",
idLine: `Your Discord user id: ${user.id}`,
code,
}),
{ ephemeral: true },
);
}
} else {
await respond("You are not authorized to use this command.", { ephemeral: true });
}
return;
}
commandAuthorized = true;
}
}
if (!isDirectMessage) {
const channelUsers = channelConfig?.users ?? guildInfo?.users;
if (Array.isArray(channelUsers) && channelUsers.length > 0) {
const userOk = resolveDiscordUserAllowed({
allowList: channelUsers,
userId: user.id,
userName: user.username,
userTag: formatDiscordUserTag(user),
});
if (!userOk) {
await respond("You are not authorized to use this command.");
return;
}
}
}
if (isGroupDm && discordConfig?.dm?.groupEnabled === false) {
await respond("Discord group DMs are disabled.");
return;
}
const menu = resolveCommandArgMenu({
command,
args: commandArgs,
cfg,
});
if (menu) {
const menuPayload = buildDiscordCommandArgMenu({
command,
menu,
interaction: interaction as CommandInteraction,
cfg,
discordConfig,
accountId,
sessionPrefix,
});
if (preferFollowUp) {
await interaction.followUp({
content: menuPayload.content,
components: menuPayload.components,
ephemeral: true,
});
return;
}
await interaction.reply({
content: menuPayload.content,
components: menuPayload.components,
ephemeral: true,
});
return;
}
const isGuild = Boolean(interaction.guild);
const channelId = channel?.id ?? "unknown";
const interactionId = interaction.rawData.id;
const route = resolveAgentRoute({
cfg,
channel: "discord",
accountId,
guildId: interaction.guild?.id ?? undefined,
peer: {
kind: isDirectMessage ? "dm" : isGroupDm ? "group" : "channel",
id: isDirectMessage ? user.id : channelId,
},
});
const ctxPayload = {
Body: prompt,
CommandBody: prompt,
CommandArgs: commandArgs,
From: isDirectMessage ? `discord:${user.id}` : `group:${channelId}`,
To: `slash:${user.id}`,
SessionKey: `agent:${route.agentId}:${sessionPrefix}:${user.id}`,
CommandTargetSessionKey: route.sessionKey,
AccountId: route.accountId,
ChatType: isDirectMessage ? "direct" : "group",
GroupSubject: isGuild ? interaction.guild?.name : undefined,
GroupSystemPrompt: isGuild
? (() => {
const channelTopic =
channel && "topic" in channel ? (channel.topic ?? undefined) : undefined;
const channelDescription = channelTopic?.trim();
const systemPromptParts = [
channelDescription ? `Channel topic: ${channelDescription}` : null,
channelConfig?.systemPrompt?.trim() || null,
].filter((entry): entry is string => Boolean(entry));
return systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
})()
: undefined,
SenderName: user.globalName ?? user.username,
SenderId: user.id,
SenderUsername: user.username,
SenderTag: formatDiscordUserTag(user),
Provider: "discord" as const,
Surface: "discord" as const,
WasMentioned: true,
MessageSid: interactionId,
Timestamp: Date.now(),
CommandAuthorized: commandAuthorized,
CommandSource: "native" as const,
};
let didReply = false;
await dispatchReplyWithDispatcher({
ctx: ctxPayload,
cfg,
dispatcherOptions: {
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => {
await deliverDiscordInteractionReply({
interaction,
payload,
textLimit: resolveTextChunkLimit(cfg, "discord", accountId, {
fallbackLimit: 2000,
}),
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
preferFollowUp: preferFollowUp || didReply,
});
didReply = true;
},
onError: (err, info) => {
console.error(`discord slash ${info.kind} reply failed`, err);
},
},
replyOptions: {
skillFilter: channelConfig?.skills,
disableBlockStreaming:
typeof discordConfig?.blockStreaming === "boolean"
? !discordConfig.blockStreaming
: undefined,
},
});
}
async function deliverDiscordInteractionReply(params: {
interaction: CommandInteraction;
interaction: CommandInteraction | ButtonInteraction;
payload: ReplyPayload;
textLimit: number;
maxLinesPerMessage?: number;