From 05ab14708153eb9d63d64fecd337313b793bd4cc Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 16 Feb 2026 13:37:43 -0500 Subject: [PATCH] Slack: expand advanced modal controls payloads and confirms --- src/slack/monitor/events/interactions.test.ts | 27 +++++++++++++++-- src/slack/monitor/events/interactions.ts | 30 +++++++++++++++---- src/slack/monitor/slash.test.ts | 6 +++- src/slack/monitor/slash.ts | 15 ++++++++++ 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/slack/monitor/events/interactions.test.ts b/src/slack/monitor/events/interactions.test.ts index 125d125f863..c94f10d048f 100644 --- a/src/slack/monitor/events/interactions.test.ts +++ b/src/slack/monitor/events/interactions.test.ts @@ -504,6 +504,9 @@ describe("registerSlackInteractionEvents", () => { const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as { actionType: string; selectedValues?: string[]; + selectedUsers?: string[]; + selectedChannels?: string[]; + selectedConversations?: string[]; selectedLabels?: string[]; selectedDate?: string; selectedTime?: string; @@ -520,6 +523,9 @@ describe("registerSlackInteractionEvents", () => { "G777", "G888", ]); + expect(payload.selectedUsers).toEqual(["U777", "U888"]); + expect(payload.selectedChannels).toEqual(["C777", "C888"]); + expect(payload.selectedConversations).toEqual(["G777", "G888"]); expect(payload.selectedLabels).toEqual(["Alpha", "Beta"]); expect(payload.selectedDate).toBe("2026-02-16"); expect(payload.selectedTime).toBe("14:30"); @@ -719,6 +725,9 @@ describe("registerSlackInteractionEvents", () => { actionId: string; inputKind?: string; selectedValues?: string[]; + selectedUsers?: string[]; + selectedChannels?: string[]; + selectedConversations?: string[]; selectedLabels?: string[]; selectedDate?: string; selectedTime?: string; @@ -736,9 +745,21 @@ describe("registerSlackInteractionEvents", () => { selectedValues: ["prod"], selectedLabels: ["Production"], }), - expect.objectContaining({ actionId: "assignee_select", selectedValues: ["U900"] }), - expect.objectContaining({ actionId: "channel_select", selectedValues: ["C900"] }), - expect.objectContaining({ actionId: "convo_select", selectedValues: ["G900"] }), + expect.objectContaining({ + actionId: "assignee_select", + selectedValues: ["U900"], + selectedUsers: ["U900"], + }), + expect.objectContaining({ + actionId: "channel_select", + selectedValues: ["C900"], + selectedChannels: ["C900"], + }), + expect.objectContaining({ + actionId: "convo_select", + selectedValues: ["G900"], + selectedConversations: ["G900"], + }), expect.objectContaining({ actionId: "date_select", selectedDate: "2026-02-16" }), expect.objectContaining({ actionId: "time_select", selectedTime: "12:45" }), expect.objectContaining({ actionId: "datetime_select", selectedDateTime: 1_771_632_300 }), diff --git a/src/slack/monitor/events/interactions.ts b/src/slack/monitor/events/interactions.ts index 93b6a1aaef8..c2ee8f52337 100644 --- a/src/slack/monitor/events/interactions.ts +++ b/src/slack/monitor/events/interactions.ts @@ -26,6 +26,9 @@ type InteractionSummary = { inputKind?: "text" | "number" | "email" | "url" | "rich_text"; value?: string; selectedValues?: string[]; + selectedUsers?: string[]; + selectedChannels?: string[]; + selectedConversations?: string[]; selectedLabels?: string[]; selectedDate?: string; selectedTime?: string; @@ -51,6 +54,9 @@ type ModalInputSummary = { inputKind?: "text" | "number" | "email" | "url" | "rich_text"; value?: string; selectedValues?: string[]; + selectedUsers?: string[]; + selectedChannels?: string[]; + selectedConversations?: string[]; selectedLabels?: string[]; selectedDate?: string; selectedTime?: string; @@ -121,15 +127,24 @@ function summarizeAction( rich_text_value?: unknown; }; const actionType = typed.type; + const selectedUsers = uniqueNonEmptyStrings([ + ...(typed.selected_user ? [typed.selected_user] : []), + ...(Array.isArray(typed.selected_users) ? typed.selected_users : []), + ]); + const selectedChannels = uniqueNonEmptyStrings([ + ...(typed.selected_channel ? [typed.selected_channel] : []), + ...(Array.isArray(typed.selected_channels) ? typed.selected_channels : []), + ]); + const selectedConversations = uniqueNonEmptyStrings([ + ...(typed.selected_conversation ? [typed.selected_conversation] : []), + ...(Array.isArray(typed.selected_conversations) ? typed.selected_conversations : []), + ]); const selectedValues = uniqueNonEmptyStrings([ ...(typed.selected_option?.value ? [typed.selected_option.value] : []), ...(readOptionValues(typed.selected_options) ?? []), - ...(typed.selected_user ? [typed.selected_user] : []), - ...(Array.isArray(typed.selected_users) ? typed.selected_users : []), - ...(typed.selected_channel ? [typed.selected_channel] : []), - ...(Array.isArray(typed.selected_channels) ? typed.selected_channels : []), - ...(typed.selected_conversation ? [typed.selected_conversation] : []), - ...(Array.isArray(typed.selected_conversations) ? typed.selected_conversations : []), + ...selectedUsers, + ...selectedChannels, + ...selectedConversations, ]); const selectedLabels = uniqueNonEmptyStrings([ ...(typed.selected_option?.text?.text ? [typed.selected_option.text.text] : []), @@ -169,6 +184,9 @@ function summarizeAction( inputKind, value: typed.value, selectedValues: selectedValues.length > 0 ? selectedValues : undefined, + selectedUsers: selectedUsers.length > 0 ? selectedUsers : undefined, + selectedChannels: selectedChannels.length > 0 ? selectedChannels : undefined, + selectedConversations: selectedConversations.length > 0 ? selectedConversations : undefined, selectedLabels: selectedLabels.length > 0 ? selectedLabels : undefined, selectedDate: typed.selected_date, selectedTime: typed.selected_time, diff --git a/src/slack/monitor/slash.test.ts b/src/slack/monitor/slash.test.ts index d6a949ff213..9ed16ae2bd1 100644 --- a/src/slack/monitor/slash.test.ts +++ b/src/slack/monitor/slash.test.ts @@ -169,7 +169,7 @@ function encodeValue(parts: { command: string; arg: string; value: string; userI function findFirstActionsBlock(payload: { blocks?: Array<{ type: string }> }) { return payload.blocks?.find((block) => block.type === "actions") as - | { type: string; elements?: Array<{ type?: string; action_id?: string }> } + | { type: string; elements?: Array<{ type?: string; action_id?: string; confirm?: unknown }> } | undefined; } @@ -293,6 +293,7 @@ describe("Slack native command argument menus", () => { const actions = findFirstActionsBlock(payload); const elementType = actions?.elements?.[0]?.type; expect(elementType).toBe("button"); + expect(actions?.elements?.[0]?.confirm).toBeTruthy(); }); it("shows a static_select menu when choices exceed button row size", async () => { @@ -321,6 +322,7 @@ describe("Slack native command argument menus", () => { const element = actions?.elements?.[0]; expect(element?.type).toBe("static_select"); expect(element?.action_id).toBe("openclaw_cmdarg"); + expect(element?.confirm).toBeTruthy(); }); it("falls back to buttons when static_select value limit would be exceeded", async () => { @@ -345,6 +347,7 @@ describe("Slack native command argument menus", () => { const actions = findFirstActionsBlock(payload); const firstElement = actions?.elements?.[0]; expect(firstElement?.type).toBe("button"); + expect(firstElement?.confirm).toBeTruthy(); }); it("shows an overflow menu when choices fit compact range", async () => { @@ -370,6 +373,7 @@ describe("Slack native command argument menus", () => { const element = actions?.elements?.[0]; expect(element?.type).toBe("overflow"); expect(element?.action_id).toBe("openclaw_cmdarg"); + expect(element?.confirm).toBeTruthy(); }); it("dispatches the command when a menu button is clicked", async () => { diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index e2844407ed0..b512539b08e 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -47,6 +47,18 @@ function truncatePlainText(value: string, max: number): string { return `${trimmed.slice(0, max - 1)}…`; } +function buildSlackArgMenuConfirm(params: { command: string; arg: string }) { + return { + title: { type: "plain_text", text: "Confirm selection" }, + text: { + type: "mrkdwn", + text: `Run */${params.command}* with *${params.arg}* set to this value?`, + }, + confirm: { type: "plain_text", text: "Run command" }, + deny: { type: "plain_text", text: "Cancel" }, + }; +} + type CommandsRegistry = typeof import("../../auto-reply/commands-registry.js"); let commandsRegistry: CommandsRegistry | undefined; async function getCommandsRegistry(): Promise { @@ -141,6 +153,7 @@ function buildSlackCommandArgMenuBlocks(params: { { type: "overflow", action_id: SLACK_COMMAND_ARG_ACTION_ID, + confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), options: encodedChoices.map((choice) => ({ text: { type: "plain_text", text: choice.label.slice(0, 75) }, value: choice.value, @@ -157,6 +170,7 @@ function buildSlackCommandArgMenuBlocks(params: { action_id: SLACK_COMMAND_ARG_ACTION_ID, text: { type: "plain_text", text: choice.label }, value: choice.value, + confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), })), })) : chunkItems(encodedChoices, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX).map((choices, index) => ({ @@ -165,6 +179,7 @@ function buildSlackCommandArgMenuBlocks(params: { { type: "static_select", action_id: SLACK_COMMAND_ARG_ACTION_ID, + confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), placeholder: { type: "plain_text", text: index === 0 ? `Choose ${params.arg}` : `Choose ${params.arg} (${index + 1})`,