diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a2c034324..e5018c98b63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai - Slack/interactive replies: drop overlong Block Kit button URLs while preserving valid callback values, so malformed link buttons no longer make Slack reject the whole interactive reply. Thanks @slackapi. - Slack/commands: truncate native command argument-menu confirmation text to Slack's dialog limit, so long plugin arg names no longer make fallback buttons render invalid Block Kit payloads. Thanks @slackapi. - Slack/exec approvals: cap native approval metadata context to Slack's element and text limits, so large approval details no longer make Slack reject the approval card. Thanks @slackapi. +- Slack/commands: cap native command argument-menu fallback rows to Slack's message block limit, so large plugin choice lists no longer make Slack reject the generated menu. Thanks @slackapi. - Channels/WhatsApp: require Baileys outbound message ids before marking auto-replies delivered, so transcript text and ack reactions no longer make failed group replies look sent. Fixes #49225. Thanks @TinyTb. - CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash. - Channels/Voice call: keep pre-auth webhook in-flight limiting active when socket remote address metadata is missing, so slow-body requests from stripped-IP proxy paths still share the fallback bucket. (#74453) Thanks @davidangularme. diff --git a/extensions/slack/src/monitor/slash.test.ts b/extensions/slack/src/monitor/slash.test.ts index 4c851bfe670..0bbb5e7a594 100644 --- a/extensions/slack/src/monitor/slash.test.ts +++ b/extensions/slack/src/monitor/slash.test.ts @@ -8,6 +8,7 @@ vi.mock("./slash-commands.runtime.js", () => { const reportExternalCommand = { key: "reportexternal", nativeName: "reportexternal" }; const reportLongCommand = { key: "reportlong", nativeName: "reportlong" }; const reportLongButtonCommand = { key: "reportlongbutton", nativeName: "reportlongbutton" }; + const reportHugeButtonCommand = { key: "reporthugebutton", nativeName: "reporthugebutton" }; const unsafeConfirmCommand = { key: "unsafeconfirm", nativeName: "unsafeconfirm" }; const longConfirmCommand = { key: "longconfirm", nativeName: "longconfirm" }; const statusAliasCommand = { key: "status", nativeName: "status" }; @@ -76,6 +77,9 @@ vi.mock("./slash-commands.runtime.js", () => { if (normalized === "reportlongbutton") { return reportLongButtonCommand; } + if (normalized === "reporthugebutton") { + return reportHugeButtonCommand; + } if (normalized === "unsafeconfirm") { return unsafeConfirmCommand; } @@ -124,6 +128,12 @@ vi.mock("./slash-commands.runtime.js", () => { acceptsArgs: true, args: [], }, + { + name: "reporthugebutton", + description: "ReportHugeButton", + acceptsArgs: true, + args: [], + }, { name: "unsafeconfirm", description: "UnsafeConfirm", @@ -168,6 +178,15 @@ vi.mock("./slash-commands.runtime.js", () => { }, ]); } + if (params.command?.key === "reporthugebutton") { + return resolvePeriodMenu( + params, + Array.from({ length: 250 }, (_v, i) => ({ + value: `${String(i + 1)}-${"x".repeat(170)}`, + label: `Long button label ${i + 1}`, + })), + ); + } if (params.command?.key === "reportcompact") { return resolvePeriodMenu(params, baseReportPeriodChoices); } @@ -446,6 +465,7 @@ describe("Slack native command argument menus", () => { let reportExternalHandler: (args: unknown) => Promise; let reportLongHandler: (args: unknown) => Promise; let reportLongButtonHandler: (args: unknown) => Promise; + let reportHugeButtonHandler: (args: unknown) => Promise; let unsafeConfirmHandler: (args: unknown) => Promise; let longConfirmHandler: (args: unknown) => Promise; let agentStatusHandler: (args: unknown) => Promise; @@ -465,6 +485,11 @@ describe("Slack native command argument menus", () => { "/reportlongbutton", "/reportlongbutton", ); + reportHugeButtonHandler = requireHandler( + harness.commands, + "/reporthugebutton", + "/reporthugebutton", + ); unsafeConfirmHandler = requireHandler(harness.commands, "/unsafeconfirm", "/unsafeconfirm"); longConfirmHandler = requireHandler(harness.commands, "/longconfirm", "/longconfirm"); agentStatusHandler = requireHandler(harness.commands, "/agentstatus", "/agentstatus"); @@ -601,6 +626,18 @@ describe("Slack native command argument menus", () => { expect(firstElement?.confirm).toBeTruthy(); }); + it("caps large button fallback menus to Slack's block limit", async () => { + const { respond } = await runCommandHandler(reportHugeButtonHandler); + expect(respond).toHaveBeenCalledTimes(1); + const payload = respond.mock.calls[0]?.[0] as { + blocks?: Array<{ type: string; elements?: unknown[] }>; + }; + const actionBlocks = (payload.blocks ?? []).filter((block) => block.type === "actions"); + expect(payload.blocks).toHaveLength(50); + expect(actionBlocks).toHaveLength(47); + expect(actionBlocks.at(-1)?.elements).toHaveLength(5); + }); + it("shows an overflow menu when choices fit compact range", async () => { const element = await getFirstActionElementFromCommand(reportCompactHandler); expect(element?.type).toBe("overflow"); diff --git a/extensions/slack/src/monitor/slash.ts b/extensions/slack/src/monitor/slash.ts index b551bd1f85a..9e002b38206 100644 --- a/extensions/slack/src/monitor/slash.ts +++ b/extensions/slack/src/monitor/slash.ts @@ -26,6 +26,7 @@ import { normalizeOptionalString, } from "openclaw/plugin-sdk/text-runtime"; import type { ResolvedSlackAccount } from "../accounts.js"; +import { SLACK_MAX_BLOCKS } from "../blocks-input.js"; import { truncateSlackText } from "../truncate.js"; import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "./allow-list.js"; import { resolveSlackEffectiveAllowFrom } from "./auth.js"; @@ -57,6 +58,8 @@ const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75; const SLACK_COMMAND_ARG_BUTTON_TEXT_MAX = 75; const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300; const SLACK_HEADER_TEXT_MAX = 150; +const SLACK_COMMAND_ARG_CHROME_BLOCKS = 3; +const SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX = SLACK_MAX_BLOCKS - SLACK_COMMAND_ARG_CHROME_BLOCKS; let slashCommandsRuntimePromise: Promise | null = null; let slashDispatchRuntimePromise: Promise | null = @@ -338,6 +341,7 @@ function buildSlackCommandArgMenuBlocks(params: { `Select one option to continue /${params.command} (${params.arg})`, 3000, ); + const visibleRows = rows.slice(0, SLACK_COMMAND_ARG_ACTION_BLOCKS_MAX); return [ { type: "header", @@ -351,7 +355,7 @@ function buildSlackCommandArgMenuBlocks(params: { type: "context", elements: [{ type: "mrkdwn", text: contextText }], }, - ...rows, + ...visibleRows, ]; }