From fbc145440f287411919d2b1fd0f90b0ce4d548d5 Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:15:19 -0700 Subject: [PATCH] fix(slack): offset presentation controls after native blocks Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com> --- extensions/slack/src/blocks-render.ts | 7 ++- extensions/slack/src/outbound-adapter.ts | 31 +++++++---- extensions/slack/src/outbound-payload.test.ts | 55 +++++++++++++++++++ 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/extensions/slack/src/blocks-render.ts b/extensions/slack/src/blocks-render.ts index 879071ef03a..a79ff542202 100644 --- a/extensions/slack/src/blocks-render.ts +++ b/extensions/slack/src/blocks-render.ts @@ -186,7 +186,10 @@ export function buildSlackInteractiveBlocks( }).blocks; } -export function buildSlackPresentationBlocks(presentation?: MessagePresentation): SlackBlock[] { +export function buildSlackPresentationBlocks( + presentation?: MessagePresentation, + options: SlackInteractiveBlockRenderOptions = {}, +): SlackBlock[] { if (!presentation) { return []; } @@ -229,6 +232,6 @@ export function buildSlackPresentationBlocks(presentation?: MessagePresentation) (block) => block.type === "buttons" || block.type === "select", ), }); - blocks.push(...buildSlackInteractiveBlocks(interactive)); + blocks.push(...buildSlackInteractiveBlocks(interactive, options)); return blocks; } diff --git a/extensions/slack/src/outbound-adapter.ts b/extensions/slack/src/outbound-adapter.ts index 5d53ee4b1bf..b30ee1dce47 100644 --- a/extensions/slack/src/outbound-adapter.ts +++ b/extensions/slack/src/outbound-adapter.ts @@ -120,7 +120,11 @@ function resolveSlackBlocks(payload: { | undefined; const nativeBlocks = parseSlackBlocksInput(slackData?.blocks) as SlackBlock[] | undefined; const renderedPresentation = - slackData?.presentationBlocks ?? buildSlackPresentationBlocks(payload.presentation); + slackData?.presentationBlocks ?? + buildSlackPresentationBlocks( + payload.presentation, + resolveSlackInteractiveBlockOffsets(nativeBlocks), + ); const previousBlocks = [...(nativeBlocks ?? []), ...renderedPresentation]; const renderedInteractive = resolveRenderedInteractiveBlocks(payload.interactive, previousBlocks); const mergedBlocks = [...previousBlocks, ...(renderedInteractive ?? [])]; @@ -147,16 +151,23 @@ export const slackOutbound: ChannelOutboundAdapter = { context: true, divider: true, }, - renderPresentation: ({ payload, presentation }) => ({ - ...payload, - channelData: { - ...payload.channelData, - slack: { - ...(payload.channelData?.slack as Record | undefined), - presentationBlocks: buildSlackPresentationBlocks(presentation), + renderPresentation: ({ payload, presentation }) => { + const slackData = payload.channelData?.slack as Record | undefined; + const nativeBlocks = parseSlackBlocksInput(slackData?.blocks) as SlackBlock[] | undefined; + return { + ...payload, + channelData: { + ...payload.channelData, + slack: { + ...slackData, + presentationBlocks: buildSlackPresentationBlocks( + presentation, + resolveSlackInteractiveBlockOffsets(nativeBlocks), + ), + }, }, - }, - }), + }; + }, sendPayload: async (ctx) => { const payload = { ...ctx.payload, diff --git a/extensions/slack/src/outbound-payload.test.ts b/extensions/slack/src/outbound-payload.test.ts index 7d54f4d14e2..b558d677b0f 100644 --- a/extensions/slack/src/outbound-payload.test.ts +++ b/extensions/slack/src/outbound-payload.test.ts @@ -94,6 +94,61 @@ describe("slackOutbound sendPayload", () => { await expect(run()).rejects.toThrow(/Slack blocks cannot exceed 50 items/i); expect(sendMock).not.toHaveBeenCalled(); }); + + it("offsets presentation controls against native Slack blocks before standalone interactive controls", async () => { + const { run, sendMock, to } = createHarness({ + payload: { + text: "Deploy?", + channelData: { + slack: { + blocks: [ + { + type: "actions", + block_id: "openclaw_reply_buttons_1", + elements: [], + }, + ], + }, + }, + presentation: { + blocks: [ + { + type: "buttons", + buttons: [{ label: "Stage", value: "stage" }], + }, + ], + }, + interactive: { + blocks: [ + { + type: "buttons", + buttons: [{ label: "Approve", value: "approve" }], + }, + ], + }, + }, + }); + + await run(); + + expect(sendMock).toHaveBeenCalledWith( + to, + "Deploy?", + expect.objectContaining({ + blocks: [ + expect.objectContaining({ block_id: "openclaw_reply_buttons_1" }), + expect.objectContaining({ + block_id: "openclaw_reply_buttons_2", + elements: [expect.objectContaining({ action_id: "openclaw:reply_button:2:1" })], + }), + expect.objectContaining({ + block_id: "openclaw_reply_buttons_3", + elements: [expect.objectContaining({ action_id: "openclaw:reply_button:3:1" })], + }), + ], + }), + ); + }); }); describe("Slack outbound payload contract", () => {