diff --git a/CHANGELOG.md b/CHANGELOG.md index c81854ae8ba..70c0833aedb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai - CLI/status: resolve read-only channel setup runtime fallback from the packaged OpenClaw dist root, so `status --all`, `status --deep`, channel, and doctor paths do not crash when an external channel plugin needs setup metadata. Fixes #74693. Thanks @giangthb. - Google Meet: block managed Chrome intro/test speech until browser health proves the participant is in-call, and expose `speechReady` diagnostics so login, admission, permission, and audio-bridge blockers no longer look like successful speech. Refs #72478. Thanks @DougButdorf. +- Slack/commands: keep native command argument menus on select controls for encoded choice values up to Slack's option limit and truncate fallback button labels to Slack's button-text limit, so long valid choices no longer render invalid Slack blocks. Thanks @slackapi. - 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. - Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc. diff --git a/extensions/slack/src/monitor/slash.test.ts b/extensions/slack/src/monitor/slash.test.ts index 6b42160e79e..60b5a884905 100644 --- a/extensions/slack/src/monitor/slash.test.ts +++ b/extensions/slack/src/monitor/slash.test.ts @@ -7,6 +7,7 @@ vi.mock("./slash-commands.runtime.js", () => { const reportCompactCommand = { key: "reportcompact", nativeName: "reportcompact" }; const reportExternalCommand = { key: "reportexternal", nativeName: "reportexternal" }; const reportLongCommand = { key: "reportlong", nativeName: "reportlong" }; + const reportLongButtonCommand = { key: "reportlongbutton", nativeName: "reportlongbutton" }; const unsafeConfirmCommand = { key: "unsafeconfirm", nativeName: "unsafeconfirm" }; const statusAliasCommand = { key: "status", nativeName: "status" }; const periodArg = { name: "period", description: "period" }; @@ -71,6 +72,9 @@ vi.mock("./slash-commands.runtime.js", () => { if (normalized === "reportlong") { return reportLongCommand; } + if (normalized === "reportlongbutton") { + return reportLongButtonCommand; + } if (normalized === "unsafeconfirm") { return unsafeConfirmCommand; } @@ -110,6 +114,12 @@ vi.mock("./slash-commands.runtime.js", () => { acceptsArgs: true, args: [], }, + { + name: "reportlongbutton", + description: "ReportLongButton", + acceptsArgs: true, + args: [], + }, { name: "unsafeconfirm", description: "UnsafeConfirm", @@ -140,6 +150,14 @@ vi.mock("./slash-commands.runtime.js", () => { { value: "x".repeat(90), label: "long" }, ]); } + if (params.command?.key === "reportlongbutton") { + return resolvePeriodMenu(params, [ + { + value: "x".repeat(170), + label: "Long button label ".repeat(8), + }, + ]); + } if (params.command?.key === "reportcompact") { return resolvePeriodMenu(params, baseReportPeriodChoices); } @@ -408,6 +426,7 @@ describe("Slack native command argument menus", () => { let reportCompactHandler: (args: unknown) => Promise; let reportExternalHandler: (args: unknown) => Promise; let reportLongHandler: (args: unknown) => Promise; + let reportLongButtonHandler: (args: unknown) => Promise; let unsafeConfirmHandler: (args: unknown) => Promise; let agentStatusHandler: (args: unknown) => Promise; let argMenuHandler: (args: unknown) => Promise; @@ -421,6 +440,11 @@ describe("Slack native command argument menus", () => { reportCompactHandler = requireHandler(harness.commands, "/reportcompact", "/reportcompact"); reportExternalHandler = requireHandler(harness.commands, "/reportexternal", "/reportexternal"); reportLongHandler = requireHandler(harness.commands, "/reportlong", "/reportlong"); + reportLongButtonHandler = requireHandler( + harness.commands, + "/reportlongbutton", + "/reportlongbutton", + ); unsafeConfirmHandler = requireHandler(harness.commands, "/unsafeconfirm", "/unsafeconfirm"); agentStatusHandler = requireHandler(harness.commands, "/agentstatus", "/agentstatus"); argMenuHandler = requireHandler(harness.actions, /^openclaw_cmdarg/, "arg-menu action"); @@ -539,9 +563,20 @@ describe("Slack native command argument menus", () => { expect(element?.confirm).toBeTruthy(); }); - it("falls back to buttons when static_select value limit would be exceeded", async () => { + it("uses static_select when encoded values fit Slack option limits", async () => { const firstElement = await getFirstActionElementFromCommand(reportLongHandler); + expect(firstElement?.type).toBe("static_select"); + expect(firstElement?.confirm).toBeTruthy(); + }); + + it("truncates button labels when static_select value limit would be exceeded", async () => { + const firstElement = (await getFirstActionElementFromCommand(reportLongButtonHandler)) as + | { type?: string; text?: { text?: string }; value?: string; confirm?: unknown } + | undefined; expect(firstElement?.type).toBe("button"); + expect(firstElement?.text?.text).toHaveLength(75); + expect(firstElement?.text?.text?.endsWith("…")).toBe(true); + expect(firstElement?.value?.length).toBeGreaterThan(150); expect(firstElement?.confirm).toBeTruthy(); }); diff --git a/extensions/slack/src/monitor/slash.ts b/extensions/slack/src/monitor/slash.ts index bf927a064fc..817fde8dfe2 100644 --- a/extensions/slack/src/monitor/slash.ts +++ b/extensions/slack/src/monitor/slash.ts @@ -52,7 +52,9 @@ const SLACK_COMMAND_ARG_BUTTON_ROW_SIZE = 5; 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_COMMAND_ARG_SELECT_OPTION_TEXT_MAX = 75; +const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 150; +const SLACK_COMMAND_ARG_BUTTON_TEXT_MAX = 75; const SLACK_HEADER_TEXT_MAX = 150; let slashCommandsRuntimePromise: Promise | null = null; @@ -217,7 +219,10 @@ function parseSlackCommandArgValue(raw?: string | null): { function buildSlackArgMenuOptions(choices: EncodedMenuChoice[]) { return choices.map((choice) => ({ - text: { type: "plain_text", text: choice.label.slice(0, 75) }, + text: { + type: "plain_text", + text: truncateSlackText(choice.label, SLACK_COMMAND_ARG_SELECT_OPTION_TEXT_MAX), + }, value: choice.value, })); } @@ -293,7 +298,10 @@ function buildSlackCommandArgMenuBlocks(params: { elements: choices.map((choice, colIndex) => ({ type: "button", action_id: `${SLACK_COMMAND_ARG_ACTION_ID}_${rowIndex}_${colIndex}`, - text: { type: "plain_text", text: choice.label }, + text: { + type: "plain_text", + text: truncateSlackText(choice.label, SLACK_COMMAND_ARG_BUTTON_TEXT_MAX), + }, value: choice.value, confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), })),