fix(slack): offset presentation controls after native blocks

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
clawsweeper[bot]
2026-04-29 23:15:19 -07:00
committed by GitHub
parent a265abaf29
commit fbc145440f
3 changed files with 81 additions and 12 deletions

View File

@@ -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;
}

View File

@@ -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<string, unknown> | undefined),
presentationBlocks: buildSlackPresentationBlocks(presentation),
renderPresentation: ({ payload, presentation }) => {
const slackData = payload.channelData?.slack as Record<string, unknown> | 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,

View File

@@ -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", () => {