Commands: add dynamic arg menus
This commit is contained in:
committed by
Peter Steinberger
parent
7e1e7ba2d8
commit
74bc5bfd7c
@@ -1,8 +1,13 @@
|
||||
import type { SlackCommandMiddlewareArgs } from "@slack/bolt";
|
||||
import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt";
|
||||
import type { Block, KnownBlock } from "@slack/types";
|
||||
import type { ChatCommandDefinition, CommandArgs } from "../../auto-reply/commands-registry.js";
|
||||
import { resolveEffectiveMessagesConfig } from "../../agents/identity.js";
|
||||
import {
|
||||
buildCommandText,
|
||||
buildCommandTextFromArgs,
|
||||
findCommandByNativeName,
|
||||
listNativeCommandSpecsForConfig,
|
||||
parseCommandArgs,
|
||||
resolveCommandArgMenu,
|
||||
} from "../../auto-reply/commands-registry.js";
|
||||
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
||||
import { resolveNativeCommandsEnabled } from "../../config/commands.js";
|
||||
@@ -28,6 +33,82 @@ import type { SlackMonitorContext } from "./context.js";
|
||||
import { isSlackRoomAllowedByPolicy } from "./policy.js";
|
||||
import { deliverSlackSlashReplies } from "./replies.js";
|
||||
|
||||
const SLACK_COMMAND_ARG_ACTION_ID = "clawdbot_cmdarg";
|
||||
const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function encodeSlackCommandArgValue(parts: {
|
||||
command: string;
|
||||
arg: string;
|
||||
value: string;
|
||||
userId: string;
|
||||
}) {
|
||||
return [
|
||||
SLACK_COMMAND_ARG_VALUE_PREFIX,
|
||||
encodeURIComponent(parts.command),
|
||||
encodeURIComponent(parts.arg),
|
||||
encodeURIComponent(parts.value),
|
||||
encodeURIComponent(parts.userId),
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function parseSlackCommandArgValue(raw?: string | null): {
|
||||
command: string;
|
||||
arg: string;
|
||||
value: string;
|
||||
userId: string;
|
||||
} | null {
|
||||
if (!raw) return null;
|
||||
const parts = raw.split("|");
|
||||
if (parts.length < 5 || parts[0] !== SLACK_COMMAND_ARG_VALUE_PREFIX) return null;
|
||||
const [, command, arg, value, userId] = parts;
|
||||
if (!command || !arg || !value || !userId) return null;
|
||||
return {
|
||||
command: decodeURIComponent(command),
|
||||
arg: decodeURIComponent(arg),
|
||||
value: decodeURIComponent(value),
|
||||
userId: decodeURIComponent(userId),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSlackCommandArgMenuBlocks(params: {
|
||||
title: string;
|
||||
command: string;
|
||||
arg: string;
|
||||
choices: string[];
|
||||
userId: string;
|
||||
}) {
|
||||
const rows = chunkItems(params.choices, 5).map((choices) => ({
|
||||
type: "actions",
|
||||
elements: choices.map((choice) => ({
|
||||
type: "button",
|
||||
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
||||
text: { type: "plain_text", text: choice },
|
||||
value: encodeSlackCommandArgValue({
|
||||
command: params.command,
|
||||
arg: params.arg,
|
||||
value: choice,
|
||||
userId: params.userId,
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
return [
|
||||
{
|
||||
type: "section",
|
||||
text: { type: "mrkdwn", text: params.title },
|
||||
},
|
||||
...rows,
|
||||
];
|
||||
}
|
||||
|
||||
export function registerSlackMonitorSlashCommands(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
account: ResolvedSlackAccount;
|
||||
@@ -45,8 +126,10 @@ export function registerSlackMonitorSlashCommands(params: {
|
||||
ack: SlackCommandMiddlewareArgs["ack"];
|
||||
respond: SlackCommandMiddlewareArgs["respond"];
|
||||
prompt: string;
|
||||
commandArgs?: CommandArgs;
|
||||
commandDefinition?: ChatCommandDefinition;
|
||||
}) => {
|
||||
const { command, ack, respond, prompt } = p;
|
||||
const { command, ack, respond, prompt, commandArgs, commandDefinition } = p;
|
||||
try {
|
||||
if (!prompt.trim()) {
|
||||
await ack({
|
||||
@@ -183,6 +266,32 @@ export function registerSlackMonitorSlashCommands(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandDefinition) {
|
||||
const menu = resolveCommandArgMenu({
|
||||
command: commandDefinition,
|
||||
args: commandArgs,
|
||||
cfg,
|
||||
});
|
||||
if (menu) {
|
||||
const commandLabel = commandDefinition.nativeName ?? commandDefinition.key;
|
||||
const title =
|
||||
menu.title ?? `Choose ${menu.arg.description || menu.arg.name} for /${commandLabel}.`;
|
||||
const blocks = buildSlackCommandArgMenuBlocks({
|
||||
title,
|
||||
command: commandLabel,
|
||||
arg: menu.arg.name,
|
||||
choices: menu.choices,
|
||||
userId: command.user_id,
|
||||
});
|
||||
await respond({
|
||||
text: title,
|
||||
blocks,
|
||||
response_type: "ephemeral",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const channelName = channelInfo?.name;
|
||||
const roomLabel = channelName ? `#${channelName}` : `#${command.channel_id}`;
|
||||
const isRoomish = isRoom || isGroupDm;
|
||||
@@ -211,6 +320,7 @@ export function registerSlackMonitorSlashCommands(params: {
|
||||
|
||||
const ctxPayload = {
|
||||
Body: prompt,
|
||||
CommandArgs: commandArgs,
|
||||
From: isDirectMessage
|
||||
? `slack:${command.user_id}`
|
||||
: isRoom
|
||||
@@ -283,8 +393,26 @@ export function registerSlackMonitorSlashCommands(params: {
|
||||
ctx.app.command(
|
||||
`/${command.name}`,
|
||||
async ({ command: cmd, ack, respond }: SlackCommandMiddlewareArgs) => {
|
||||
const prompt = buildCommandText(command.name, cmd.text);
|
||||
await handleSlashCommand({ command: cmd, ack, respond, prompt });
|
||||
const commandDefinition = findCommandByNativeName(command.name);
|
||||
const rawText = cmd.text?.trim() ?? "";
|
||||
const commandArgs = commandDefinition
|
||||
? parseCommandArgs(commandDefinition, rawText)
|
||||
: rawText
|
||||
? ({ raw: rawText } satisfies CommandArgs)
|
||||
: undefined;
|
||||
const prompt = commandDefinition
|
||||
? buildCommandTextFromArgs(commandDefinition, commandArgs)
|
||||
: rawText
|
||||
? `/${command.name} ${rawText}`
|
||||
: `/${command.name}`;
|
||||
await handleSlashCommand({
|
||||
command: cmd,
|
||||
ack,
|
||||
respond,
|
||||
prompt,
|
||||
commandArgs,
|
||||
commandDefinition: commandDefinition ?? undefined,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -303,4 +431,70 @@ export function registerSlackMonitorSlashCommands(params: {
|
||||
} else {
|
||||
logVerbose("slack: slash commands disabled");
|
||||
}
|
||||
|
||||
ctx.app.action(SLACK_COMMAND_ARG_ACTION_ID, async (args: SlackActionMiddlewareArgs) => {
|
||||
const { ack, body, respond } = args;
|
||||
const action = args.action as { value?: string };
|
||||
await ack();
|
||||
const respondFn =
|
||||
respond ??
|
||||
(async (payload: {
|
||||
text: string;
|
||||
blocks?: (Block | KnownBlock)[];
|
||||
response_type?: string;
|
||||
}) => {
|
||||
if (!body.channel?.id || !body.user?.id) return;
|
||||
await ctx.app.client.chat.postEphemeral({
|
||||
channel: body.channel.id,
|
||||
user: body.user.id,
|
||||
text: payload.text,
|
||||
blocks: payload.blocks,
|
||||
});
|
||||
});
|
||||
const parsed = parseSlackCommandArgValue(action?.value);
|
||||
if (!parsed) {
|
||||
await respondFn({
|
||||
text: "Sorry, that button is no longer valid.",
|
||||
response_type: "ephemeral",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (body.user?.id && parsed.userId !== body.user.id) {
|
||||
await respondFn({
|
||||
text: "That menu is for another user.",
|
||||
response_type: "ephemeral",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const commandDefinition = findCommandByNativeName(parsed.command);
|
||||
const commandArgs: CommandArgs = {
|
||||
values: { [parsed.arg]: parsed.value },
|
||||
};
|
||||
const prompt = commandDefinition
|
||||
? buildCommandTextFromArgs(commandDefinition, commandArgs)
|
||||
: `/${parsed.command} ${parsed.value}`;
|
||||
const user = body.user;
|
||||
const userName =
|
||||
user && "name" in user && user.name
|
||||
? user.name
|
||||
: user && "username" in user && user.username
|
||||
? user.username
|
||||
: (user?.id ?? "");
|
||||
const triggerId = "trigger_id" in body ? body.trigger_id : undefined;
|
||||
const commandPayload = {
|
||||
user_id: user?.id ?? "",
|
||||
user_name: userName,
|
||||
channel_id: body.channel?.id ?? "",
|
||||
channel_name: body.channel?.name ?? body.channel?.id ?? "",
|
||||
trigger_id: triggerId ?? String(Date.now()),
|
||||
} as SlackCommandMiddlewareArgs["command"];
|
||||
await handleSlashCommand({
|
||||
command: commandPayload,
|
||||
ack: async () => {},
|
||||
respond: respondFn as SlackCommandMiddlewareArgs["respond"],
|
||||
prompt,
|
||||
commandArgs,
|
||||
commandDefinition: commandDefinition ?? undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user