fix: restore Discord model picker UX (#21458) (thanks @pejmanjohn)

This commit is contained in:
Shadow
2026-02-20 21:03:19 -06:00
committed by Shadow
parent 5dae5e6ef2
commit b7644d61a2
10 changed files with 2871 additions and 7 deletions

View File

@@ -2,12 +2,16 @@ import {
Button,
ChannelType,
Command,
Container,
Row,
StringSelectMenu,
TextDisplay,
type AutocompleteInteraction,
type ButtonInteraction,
type CommandInteraction,
type CommandOptions,
type ComponentData,
type StringSelectMenuInteraction,
} from "@buape/carbon";
import { ApplicationCommandOptionType, ButtonStyle } from "discord-api-types/v10";
import { resolveHumanDelayConfig } from "../../agents/identity.js";
@@ -29,11 +33,14 @@ import {
serializeCommandArgs,
} from "../../auto-reply/commands-registry.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { resolveStoredModelOverride } from "../../auto-reply/reply/model-selection.js";
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
import type { ReplyPayload } from "../../auto-reply/types.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import type { OpenClawConfig, loadConfig } from "../../config/config.js";
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
import { logVerbose } from "../../globals.js";
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
import { buildPairingReply } from "../../pairing/pairing-messages.js";
import {
@@ -43,6 +50,7 @@ import {
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
import { chunkItems } from "../../utils/chunk-items.js";
import { withTimeout } from "../../utils/with-timeout.js";
import { loadWebMedia } from "../../web/media.js";
import { chunkDiscordTextWithMode } from "../chunk.js";
import {
@@ -56,6 +64,21 @@ import {
resolveDiscordOwnerAllowFrom,
} from "./allow-list.js";
import { resolveDiscordChannelInfo } from "./message-utils.js";
import {
readDiscordModelPickerRecentModels,
recordDiscordModelPickerRecentModel,
type DiscordModelPickerPreferenceScope,
} from "./model-picker-preferences.js";
import {
DISCORD_MODEL_PICKER_CUSTOM_ID_KEY,
loadDiscordModelPickerData,
parseDiscordModelPickerData,
renderDiscordModelPickerModelsView,
renderDiscordModelPickerProvidersView,
renderDiscordModelPickerRecentsView,
toDiscordModelPickerMessagePayload,
type DiscordModelPickerCommandContext,
} from "./model-picker.js";
import { resolveDiscordSenderIdentity } from "./sender-identity.js";
import { resolveDiscordThreadParentInfo } from "./threading.js";
@@ -196,7 +219,7 @@ async function safeDiscordInteractionCall<T>(
return await fn();
} catch (error) {
if (isDiscordUnknownInteraction(error)) {
console.warn(`discord: ${label} skipped (interaction expired)`);
logVerbose(`discord: ${label} skipped (interaction expired)`);
return null;
}
throw error;
@@ -247,6 +270,634 @@ type DiscordCommandArgContext = {
sessionPrefix: string;
};
type DiscordModelPickerContext = DiscordCommandArgContext;
function resolveDiscordModelPickerCommandContext(
command: ChatCommandDefinition,
): DiscordModelPickerCommandContext | null {
const normalized = (command.nativeName ?? command.key).trim().toLowerCase();
if (normalized === "model" || normalized === "models") {
return normalized;
}
return null;
}
function resolveCommandArgStringValue(args: CommandArgs | undefined, key: string): string {
const value = args?.values?.[key];
if (typeof value !== "string") {
return "";
}
return value.trim();
}
function shouldOpenDiscordModelPickerFromCommand(params: {
command: ChatCommandDefinition;
commandArgs?: CommandArgs;
}): DiscordModelPickerCommandContext | null {
const context = resolveDiscordModelPickerCommandContext(params.command);
if (!context) {
return null;
}
const serializedArgs = serializeCommandArgs(params.command, params.commandArgs)?.trim() ?? "";
if (context === "model") {
const modelValue = resolveCommandArgStringValue(params.commandArgs, "model");
return !modelValue && !serializedArgs ? context : null;
}
return serializedArgs ? null : context;
}
function buildDiscordModelPickerCurrentModel(
defaultProvider: string,
defaultModel: string,
): string {
return `${defaultProvider}/${defaultModel}`;
}
function buildDiscordModelPickerAllowedModelRefs(
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>,
): Set<string> {
const out = new Set<string>();
for (const provider of data.providers) {
const models = data.byProvider.get(provider);
if (!models) {
continue;
}
for (const model of models) {
out.add(`${provider}/${model}`);
}
}
return out;
}
function resolveDiscordModelPickerPreferenceScope(params: {
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
accountId: string;
userId: string;
}): DiscordModelPickerPreferenceScope {
return {
accountId: params.accountId,
guildId: params.interaction.guild?.id ?? undefined,
userId: params.userId,
};
}
function buildDiscordModelPickerNoticePayload(message: string): { components: Container[] } {
return {
components: [new Container([new TextDisplay(message)])],
};
}
async function resolveDiscordModelPickerRoute(params: {
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
cfg: ReturnType<typeof loadConfig>;
accountId: string;
}) {
const { interaction, cfg, accountId } = params;
const channel = interaction.channel;
const channelType = channel?.type;
const isDirectMessage = channelType === ChannelType.DM;
const isGroupDm = channelType === ChannelType.GroupDM;
const isThreadChannel =
channelType === ChannelType.PublicThread ||
channelType === ChannelType.PrivateThread ||
channelType === ChannelType.AnnouncementThread;
const rawChannelId = channel?.id ?? "unknown";
const memberRoleIds = Array.isArray(interaction.rawData.member?.roles)
? interaction.rawData.member.roles.map((roleId: string) => String(roleId))
: [];
let threadParentId: string | undefined;
if (interaction.guild && channel && isThreadChannel && rawChannelId) {
const channelInfo = await resolveDiscordChannelInfo(interaction.client, rawChannelId);
const parentInfo = await resolveDiscordThreadParentInfo({
client: interaction.client,
threadChannel: {
id: rawChannelId,
name: "name" in channel ? (channel.name as string | undefined) : undefined,
parentId: "parentId" in channel ? (channel.parentId ?? undefined) : undefined,
parent: undefined,
},
channelInfo,
});
threadParentId = parentInfo.id;
}
return resolveAgentRoute({
cfg,
channel: "discord",
accountId,
guildId: interaction.guild?.id ?? undefined,
memberRoleIds,
peer: {
kind: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
id: isDirectMessage ? (interaction.user?.id ?? rawChannelId) : rawChannelId,
},
parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
});
}
function resolveDiscordModelPickerCurrentModel(params: {
cfg: ReturnType<typeof loadConfig>;
route: ReturnType<typeof resolveAgentRoute>;
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>;
}): string {
const fallback = buildDiscordModelPickerCurrentModel(
params.data.resolvedDefault.provider,
params.data.resolvedDefault.model,
);
try {
const storePath = resolveStorePath(params.cfg.session?.store, {
agentId: params.route.agentId,
});
const sessionStore = loadSessionStore(storePath, { skipCache: true });
const sessionEntry = sessionStore[params.route.sessionKey];
const override = resolveStoredModelOverride({
sessionEntry,
sessionStore,
sessionKey: params.route.sessionKey,
});
if (!override?.model) {
return fallback;
}
const provider = (override.provider || params.data.resolvedDefault.provider).trim();
if (!provider) {
return fallback;
}
return `${provider}/${override.model}`;
} catch {
return fallback;
}
}
async function replyWithDiscordModelPickerProviders(params: {
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
cfg: ReturnType<typeof loadConfig>;
command: DiscordModelPickerCommandContext;
userId: string;
accountId: string;
preferFollowUp: boolean;
}) {
const data = await loadDiscordModelPickerData(params.cfg);
const route = await resolveDiscordModelPickerRoute({
interaction: params.interaction,
cfg: params.cfg,
accountId: params.accountId,
});
const currentModel = resolveDiscordModelPickerCurrentModel({
cfg: params.cfg,
route,
data,
});
const quickModels = await readDiscordModelPickerRecentModels({
scope: resolveDiscordModelPickerPreferenceScope({
interaction: params.interaction,
accountId: params.accountId,
userId: params.userId,
}),
allowedModelRefs: buildDiscordModelPickerAllowedModelRefs(data),
limit: 5,
});
const rendered = renderDiscordModelPickerModelsView({
command: params.command,
userId: params.userId,
data,
provider: splitDiscordModelRef(currentModel ?? "")?.provider ?? data.resolvedDefault.provider,
page: 1,
providerPage: 1,
currentModel,
quickModels,
});
const payload = {
...toDiscordModelPickerMessagePayload(rendered),
ephemeral: true,
};
await safeDiscordInteractionCall("model picker reply", async () => {
if (params.preferFollowUp) {
await params.interaction.followUp(payload);
return;
}
await params.interaction.reply(payload);
});
}
function resolveModelPickerSelectionValue(
interaction: ButtonInteraction | StringSelectMenuInteraction,
): string | null {
const rawValues = (interaction as { values?: string[] }).values;
if (!Array.isArray(rawValues) || rawValues.length === 0) {
return null;
}
const first = rawValues[0];
if (typeof first !== "string") {
return null;
}
const trimmed = first.trim();
return trimmed || null;
}
function buildDiscordModelPickerSelectionCommand(params: {
modelRef: string;
}): { command: ChatCommandDefinition; args: CommandArgs; prompt: string } | null {
const commandDefinition =
findCommandByNativeName("model", "discord") ??
listChatCommands().find((entry) => entry.key === "model");
if (!commandDefinition) {
return null;
}
const commandArgs: CommandArgs = {
values: {
model: params.modelRef,
},
raw: params.modelRef,
};
return {
command: commandDefinition,
args: commandArgs,
prompt: buildCommandTextFromArgs(commandDefinition, commandArgs),
};
}
function listDiscordModelPickerProviderModels(
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>,
provider: string,
): string[] {
const modelSet = data.byProvider.get(provider);
if (!modelSet) {
return [];
}
return [...modelSet].toSorted();
}
function resolveDiscordModelPickerModelIndex(params: {
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>;
provider: string;
model: string;
}): number | null {
const models = listDiscordModelPickerProviderModels(params.data, params.provider);
if (!models.length) {
return null;
}
const index = models.indexOf(params.model);
if (index < 0) {
return null;
}
return index + 1;
}
function resolveDiscordModelPickerModelByIndex(params: {
data: Awaited<ReturnType<typeof loadDiscordModelPickerData>>;
provider: string;
modelIndex?: number;
}): string | null {
if (!params.modelIndex || params.modelIndex < 1) {
return null;
}
const models = listDiscordModelPickerProviderModels(params.data, params.provider);
if (!models.length) {
return null;
}
return models[params.modelIndex - 1] ?? null;
}
function splitDiscordModelRef(modelRef: string): { provider: string; model: string } | null {
const trimmed = modelRef.trim();
const slashIndex = trimmed.indexOf("/");
if (slashIndex <= 0 || slashIndex >= trimmed.length - 1) {
return null;
}
const provider = trimmed.slice(0, slashIndex).trim();
const model = trimmed.slice(slashIndex + 1).trim();
if (!provider || !model) {
return null;
}
return { provider, model };
}
async function handleDiscordModelPickerInteraction(
interaction: ButtonInteraction | StringSelectMenuInteraction,
data: ComponentData,
ctx: DiscordModelPickerContext,
) {
const parsed = parseDiscordModelPickerData(data);
if (!parsed) {
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(
buildDiscordModelPickerNoticePayload(
"Sorry, that model picker interaction is no longer available.",
),
),
);
return;
}
if (interaction.user?.id && interaction.user.id !== parsed.userId) {
await safeDiscordInteractionCall("model picker ack", () => interaction.acknowledge());
return;
}
const pickerData = await loadDiscordModelPickerData(ctx.cfg);
const route = await resolveDiscordModelPickerRoute({
interaction,
cfg: ctx.cfg,
accountId: ctx.accountId,
});
const currentModelRef = resolveDiscordModelPickerCurrentModel({
cfg: ctx.cfg,
route,
data: pickerData,
});
const allowedModelRefs = buildDiscordModelPickerAllowedModelRefs(pickerData);
const preferenceScope = resolveDiscordModelPickerPreferenceScope({
interaction,
accountId: ctx.accountId,
userId: parsed.userId,
});
const quickModels = await readDiscordModelPickerRecentModels({
scope: preferenceScope,
allowedModelRefs,
limit: 5,
});
if (parsed.action === "recents") {
const rendered = renderDiscordModelPickerRecentsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
quickModels,
currentModel: currentModelRef,
provider: parsed.provider,
page: parsed.page,
providerPage: parsed.providerPage,
});
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(toDiscordModelPickerMessagePayload(rendered)),
);
return;
}
if (parsed.action === "back" && parsed.view === "providers") {
const rendered = renderDiscordModelPickerProvidersView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
page: parsed.page,
currentModel: currentModelRef,
});
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(toDiscordModelPickerMessagePayload(rendered)),
);
return;
}
if (parsed.action === "back" && parsed.view === "models") {
const provider =
parsed.provider ??
splitDiscordModelRef(currentModelRef ?? "")?.provider ??
pickerData.resolvedDefault.provider;
const rendered = renderDiscordModelPickerModelsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
provider,
page: parsed.page ?? 1,
providerPage: parsed.providerPage ?? 1,
currentModel: currentModelRef,
quickModels,
});
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(toDiscordModelPickerMessagePayload(rendered)),
);
return;
}
if (parsed.action === "provider") {
const selectedProvider = resolveModelPickerSelectionValue(interaction) ?? parsed.provider;
if (!selectedProvider || !pickerData.byProvider.has(selectedProvider)) {
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(
buildDiscordModelPickerNoticePayload("Sorry, that provider isn't available anymore."),
),
);
return;
}
const rendered = renderDiscordModelPickerModelsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
provider: selectedProvider,
page: 1,
providerPage: parsed.providerPage ?? parsed.page,
currentModel: currentModelRef,
quickModels,
});
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(toDiscordModelPickerMessagePayload(rendered)),
);
return;
}
if (parsed.action === "model") {
const selectedModel = resolveModelPickerSelectionValue(interaction);
const provider = parsed.provider;
if (!provider || !selectedModel) {
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(
buildDiscordModelPickerNoticePayload("Sorry, I couldn't read that model selection."),
),
);
return;
}
const modelIndex = resolveDiscordModelPickerModelIndex({
data: pickerData,
provider,
model: selectedModel,
});
if (!modelIndex) {
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(
buildDiscordModelPickerNoticePayload("Sorry, that model isn't available anymore."),
),
);
return;
}
const modelRef = `${provider}/${selectedModel}`;
const rendered = renderDiscordModelPickerModelsView({
command: parsed.command,
userId: parsed.userId,
data: pickerData,
provider,
page: parsed.page,
providerPage: parsed.providerPage ?? 1,
currentModel: currentModelRef,
pendingModel: modelRef,
pendingModelIndex: modelIndex,
quickModels,
});
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(toDiscordModelPickerMessagePayload(rendered)),
);
return;
}
if (parsed.action === "submit" || parsed.action === "reset" || parsed.action === "quick") {
let modelRef: string | null = null;
if (parsed.action === "reset") {
modelRef = `${pickerData.resolvedDefault.provider}/${pickerData.resolvedDefault.model}`;
} else if (parsed.action === "quick") {
const slot = parsed.recentSlot ?? 0;
modelRef = slot >= 1 ? (quickModels[slot - 1] ?? null) : null;
} else if (parsed.view === "recents") {
const defaultModelRef = `${pickerData.resolvedDefault.provider}/${pickerData.resolvedDefault.model}`;
const dedupedRecents = quickModels.filter((ref) => ref !== defaultModelRef);
const slot = parsed.recentSlot ?? 0;
if (slot === 1) {
modelRef = defaultModelRef;
} else if (slot >= 2) {
modelRef = dedupedRecents[slot - 2] ?? null;
}
} else {
const provider = parsed.provider;
const selectedModel = resolveDiscordModelPickerModelByIndex({
data: pickerData,
provider: provider ?? "",
modelIndex: parsed.modelIndex,
});
modelRef = provider && selectedModel ? `${provider}/${selectedModel}` : null;
}
const parsedModelRef = modelRef ? splitDiscordModelRef(modelRef) : null;
if (
!parsedModelRef ||
!pickerData.byProvider.get(parsedModelRef.provider)?.has(parsedModelRef.model)
) {
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(
buildDiscordModelPickerNoticePayload(
"That selection expired. Please choose a model again.",
),
),
);
return;
}
const resolvedModelRef = `${parsedModelRef.provider}/${parsedModelRef.model}`;
const selectionCommand = buildDiscordModelPickerSelectionCommand({
modelRef: resolvedModelRef,
});
if (!selectionCommand) {
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(
buildDiscordModelPickerNoticePayload("Sorry, /model is unavailable right now."),
),
);
return;
}
const updateResult = await safeDiscordInteractionCall("model picker update", () =>
interaction.update(
buildDiscordModelPickerNoticePayload(`Applying model change to ${resolvedModelRef}...`),
),
);
if (updateResult === null) {
return;
}
try {
await withTimeout(
dispatchDiscordCommandInteraction({
interaction,
prompt: selectionCommand.prompt,
command: selectionCommand.command,
commandArgs: selectionCommand.args,
cfg: ctx.cfg,
discordConfig: ctx.discordConfig,
accountId: ctx.accountId,
sessionPrefix: ctx.sessionPrefix,
preferFollowUp: true,
suppressReplies: true,
}),
12000,
);
} catch (error) {
if (error instanceof Error && error.message === "timeout") {
await safeDiscordInteractionCall("model picker follow-up", () =>
interaction.followUp({
...buildDiscordModelPickerNoticePayload(
`⏳ Model change to ${resolvedModelRef} is still processing. Check /status in a few seconds.`,
),
ephemeral: true,
}),
);
return;
}
await safeDiscordInteractionCall("model picker follow-up", () =>
interaction.followUp({
...buildDiscordModelPickerNoticePayload(
`❌ Failed to apply ${resolvedModelRef}. Try /model ${resolvedModelRef} directly.`,
),
ephemeral: true,
}),
);
return;
}
const effectiveModelRef = resolveDiscordModelPickerCurrentModel({
cfg: ctx.cfg,
route,
data: pickerData,
});
const persisted = effectiveModelRef === resolvedModelRef;
if (!persisted) {
logVerbose(
`discord: model picker override mismatch — expected ${resolvedModelRef} but read ${effectiveModelRef} from session key ${route.sessionKey}`,
);
}
if (persisted) {
await recordDiscordModelPickerRecentModel({
scope: preferenceScope,
modelRef: resolvedModelRef,
limit: 5,
}).catch(() => undefined);
}
await safeDiscordInteractionCall("model picker follow-up", () =>
interaction.followUp({
...buildDiscordModelPickerNoticePayload(
persisted
? `✅ Model set to ${resolvedModelRef}.`
: `⚠️ Tried to set ${resolvedModelRef}, but current model is ${effectiveModelRef}.`,
),
ephemeral: true,
}),
);
return;
}
if (parsed.action === "cancel") {
const displayModel = currentModelRef ?? "default";
await safeDiscordInteractionCall("model picker update", () =>
interaction.update(buildDiscordModelPickerNoticePayload(` Model kept as ${displayModel}.`)),
);
return;
}
}
async function handleDiscordCommandArgInteraction(
interaction: ButtonInteraction,
data: ComponentData,
@@ -278,13 +929,13 @@ async function handleDiscordCommandArgInteraction(
);
return;
}
const updated = await safeDiscordInteractionCall("command arg update", () =>
const argUpdateResult = await safeDiscordInteractionCall("command arg update", () =>
interaction.update({
content: `✅ Selected ${parsed.value}.`,
components: [],
}),
);
if (!updated) {
if (argUpdateResult === null) {
return;
}
const commandArgs = createCommandArgsWithValue({
@@ -364,6 +1015,46 @@ export function createDiscordCommandArgFallbackButton(params: DiscordCommandArgC
return new DiscordCommandArgFallbackButton(params);
}
class DiscordModelPickerFallbackButton extends Button {
label = DISCORD_MODEL_PICKER_CUSTOM_ID_KEY;
customId = `${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:seed=btn`;
private ctx: DiscordModelPickerContext;
constructor(ctx: DiscordModelPickerContext) {
super();
this.ctx = ctx;
}
async run(interaction: ButtonInteraction, data: ComponentData) {
await handleDiscordModelPickerInteraction(interaction, data, this.ctx);
}
}
class DiscordModelPickerFallbackSelect extends StringSelectMenu {
customId = `${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:seed=sel`;
options = [];
private ctx: DiscordModelPickerContext;
constructor(ctx: DiscordModelPickerContext) {
super();
this.ctx = ctx;
}
async run(interaction: StringSelectMenuInteraction, data: ComponentData) {
await handleDiscordModelPickerInteraction(interaction, data, this.ctx);
}
}
export function createDiscordModelPickerFallbackButton(params: DiscordModelPickerContext): Button {
return new DiscordModelPickerFallbackButton(params);
}
export function createDiscordModelPickerFallbackSelect(
params: DiscordModelPickerContext,
): StringSelectMenu {
return new DiscordModelPickerFallbackSelect(params);
}
function buildDiscordCommandArgMenu(params: {
command: ChatCommandDefinition;
menu: {
@@ -479,7 +1170,7 @@ export function createDiscordNativeCommand(params: {
}
async function dispatchDiscordCommandInteraction(params: {
interaction: CommandInteraction | ButtonInteraction;
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
prompt: string;
command: ChatCommandDefinition;
commandArgs?: CommandArgs;
@@ -488,6 +1179,7 @@ async function dispatchDiscordCommandInteraction(params: {
accountId: string;
sessionPrefix: string;
preferFollowUp: boolean;
suppressReplies?: boolean;
}) {
const {
interaction,
@@ -499,6 +1191,7 @@ async function dispatchDiscordCommandInteraction(params: {
accountId,
sessionPrefix,
preferFollowUp,
suppressReplies,
} = params;
const respond = async (content: string, options?: { ephemeral?: boolean }) => {
const payload = {
@@ -719,6 +1412,22 @@ async function dispatchDiscordCommandInteraction(params: {
return;
}
const pickerCommandContext = shouldOpenDiscordModelPickerFromCommand({
command,
commandArgs,
});
if (pickerCommandContext) {
await replyWithDiscordModelPickerProviders({
interaction,
cfg,
command: pickerCommandContext,
userId: user.id,
accountId,
preferFollowUp,
});
return;
}
const isGuild = Boolean(interaction.guild);
const channelId = rawChannelId || "unknown";
const interactionId = interaction.rawData.id;
@@ -813,6 +1522,9 @@ async function dispatchDiscordCommandInteraction(params: {
...prefixOptions,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => {
if (suppressReplies) {
return;
}
try {
await deliverDiscordInteractionReply({
interaction,
@@ -827,7 +1539,7 @@ async function dispatchDiscordCommandInteraction(params: {
});
} catch (error) {
if (isDiscordUnknownInteraction(error)) {
console.warn("discord: interaction reply skipped (interaction expired)");
logVerbose("discord: interaction reply skipped (interaction expired)");
return;
}
throw error;
@@ -850,7 +1562,7 @@ async function dispatchDiscordCommandInteraction(params: {
}
async function deliverDiscordInteractionReply(params: {
interaction: CommandInteraction | ButtonInteraction;
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
payload: ReplyPayload;
mediaLocalRoots?: readonly string[];
textLimit: number;