From 5f9a04604e8a527d626db78f15238d97bad6b6c7 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 16 Feb 2026 13:26:59 -0500 Subject: [PATCH] Slack: add header and context blocks to arg menus --- src/slack/monitor/slash.test.ts | 37 ++++++++++++++++++--------------- src/slack/monitor/slash.ts | 31 ++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/slack/monitor/slash.test.ts b/src/slack/monitor/slash.test.ts index ba42bc4db21..d6a949ff213 100644 --- a/src/slack/monitor/slash.test.ts +++ b/src/slack/monitor/slash.test.ts @@ -167,6 +167,12 @@ function encodeValue(parts: { command: string; arg: string; value: string; userI ].join("|"); } +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 }> } + | undefined; +} + function createArgMenusHarness() { const commands = new Map Promise>(); const actions = new Map Promise>(); @@ -281,10 +287,11 @@ describe("Slack native command argument menus", () => { expect(respond).toHaveBeenCalledTimes(1); const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> }; - expect(payload.blocks?.[0]?.type).toBe("section"); - expect(payload.blocks?.[1]?.type).toBe("actions"); - const elementType = (payload.blocks?.[1] as { elements?: Array<{ type?: string }> } | undefined) - ?.elements?.[0]?.type; + expect(payload.blocks?.[0]?.type).toBe("header"); + expect(payload.blocks?.[1]?.type).toBe("section"); + expect(payload.blocks?.[2]?.type).toBe("context"); + const actions = findFirstActionsBlock(payload); + const elementType = actions?.elements?.[0]?.type; expect(elementType).toBe("button"); }); @@ -307,11 +314,11 @@ describe("Slack native command argument menus", () => { expect(respond).toHaveBeenCalledTimes(1); const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> }; - expect(payload.blocks?.[0]?.type).toBe("section"); - expect(payload.blocks?.[1]?.type).toBe("actions"); - const element = ( - payload.blocks?.[1] as { elements?: Array<{ type?: string; action_id?: string }> } | undefined - )?.elements?.[0]; + expect(payload.blocks?.[0]?.type).toBe("header"); + expect(payload.blocks?.[1]?.type).toBe("section"); + expect(payload.blocks?.[2]?.type).toBe("context"); + const actions = findFirstActionsBlock(payload); + const element = actions?.elements?.[0]; expect(element?.type).toBe("static_select"); expect(element?.action_id).toBe("openclaw_cmdarg"); }); @@ -335,10 +342,8 @@ describe("Slack native command argument menus", () => { expect(respond).toHaveBeenCalledTimes(1); const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> }; - expect(payload.blocks?.[1]?.type).toBe("actions"); - const firstElement = ( - payload.blocks?.[1] as { elements?: Array<{ type?: string }> } | undefined - )?.elements?.[0]; + const actions = findFirstActionsBlock(payload); + const firstElement = actions?.elements?.[0]; expect(firstElement?.type).toBe("button"); }); @@ -361,10 +366,8 @@ describe("Slack native command argument menus", () => { expect(respond).toHaveBeenCalledTimes(1); const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> }; - expect(payload.blocks?.[1]?.type).toBe("actions"); - const element = ( - payload.blocks?.[1] as { elements?: Array<{ type?: string; action_id?: string }> } | undefined - )?.elements?.[0]; + const actions = findFirstActionsBlock(payload); + const element = actions?.elements?.[0]; expect(element?.type).toBe("overflow"); expect(element?.action_id).toBe("openclaw_cmdarg"); }); diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index 336086c2228..e2844407ed0 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -34,6 +34,18 @@ const SLACK_COMMAND_ARG_OVERFLOW_MIN = 3; const SLACK_COMMAND_ARG_OVERFLOW_MAX = 5; const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100; const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75; +const SLACK_HEADER_TEXT_MAX = 150; + +function truncatePlainText(value: string, max: number): string { + const trimmed = value.trim(); + if (trimmed.length <= max) { + return trimmed; + } + if (max <= 1) { + return trimmed.slice(0, max); + } + return `${trimmed.slice(0, max - 1)}…`; +} type CommandsRegistry = typeof import("../../auto-reply/commands-registry.js"); let commandsRegistry: CommandsRegistry | undefined; @@ -164,10 +176,27 @@ function buildSlackCommandArgMenuBlocks(params: { }, ], })); + const headerText = truncatePlainText( + `/${params.command}: choose ${params.arg}`, + SLACK_HEADER_TEXT_MAX, + ); + const sectionText = truncatePlainText(params.title, 3000); + const contextText = truncatePlainText( + `Select one option to continue /${params.command} (${params.arg})`, + 3000, + ); return [ + { + type: "header", + text: { type: "plain_text", text: headerText }, + }, { type: "section", - text: { type: "mrkdwn", text: params.title }, + text: { type: "mrkdwn", text: sectionText }, + }, + { + type: "context", + elements: [{ type: "mrkdwn", text: contextText }], }, ...rows, ];