diff --git a/config/assertion-safety-baseline.txt b/config/assertion-safety-baseline.txt index 2251e0e0176e..8bdc6b31ce32 100644 --- a/config/assertion-safety-baseline.txt +++ b/config/assertion-safety-baseline.txt @@ -1164,7 +1164,7 @@ extensions/slack/src/approval-native-gates.ts 1 extensions/slack/src/approval-native.ts 2 extensions/slack/src/blocks-fallback.ts 2 extensions/slack/src/blocks-input.ts 2 -extensions/slack/src/blocks-render.ts 2 +extensions/slack/src/blocks-render.ts 1 extensions/slack/src/channel-actions.ts 1 extensions/slack/src/channel-type.ts 1 extensions/slack/src/channel.ts 7 diff --git a/extensions/slack/src/blocks-render.ts b/extensions/slack/src/blocks-render.ts index e85b4c49a262..eba71fc39f7e 100644 --- a/extensions/slack/src/blocks-render.ts +++ b/extensions/slack/src/blocks-render.ts @@ -2,7 +2,7 @@ import type { Block, KnownBlock } from "@slack/web-api"; import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime"; import { - reduceLegacyInteractiveReply, + legacyInteractiveReplyToPresentation, resolveMessagePresentationButtonAction, resolveMessagePresentationOptionAction, } from "openclaw/plugin-sdk/interactive-runtime"; @@ -248,119 +248,10 @@ export function buildSlackInteractiveBlocks( interactive?: LegacyInteractiveReply, options: SlackBlockRenderOptions = {}, ): SlackBlock[] { - const initialState = { - blocks: [] as SlackBlock[], - buttonIndex: options.buttonIndexOffset ?? 0, - selectIndex: options.selectIndexOffset ?? 0, - }; - return reduceLegacyInteractiveReply(interactive, initialState, (state, block) => { - if (block.type === "text") { - const trimmed = block.text.trim(); - if (!trimmed) { - return state; - } - state.blocks.push({ - type: "section", - text: { - type: "mrkdwn", - text: truncateSlackText(trimmed, SLACK_SECTION_TEXT_MAX), - }, - }); - return state; - } - if (block.type === "buttons") { - // Index is position in the question's options; core emits one buttons block in option order. - const elements = block.buttons - .flatMap((button, choiceIndex) => { - const target = resolveSlackButtonTarget(button, choiceIndex); - if ( - !target || - (target.kind === "link" - ? !isWithinSlackLimit(target.url, SLACK_BUTTON_URL_MAX) - : !isWithinSlackLimit(target.value, SLACK_BUTTON_VALUE_MAX)) - ) { - return []; - } - const style = resolveSlackButtonStyle(button.style); - return [ - { - type: "button" as const, - // Slack emits block_actions even for URL buttons; link-only actions must be ignored. - action_id: - target.kind === "link" - ? buildSlackReplyLinkActionId(state.buttonIndex + 1, choiceIndex) - : target.kind === "approval" - ? buildSlackApprovalButtonActionId(state.buttonIndex + 1, choiceIndex) - : target.kind === "callback" - ? buildSlackCallbackButtonActionId(state.buttonIndex + 1, choiceIndex) - : target.kind === "question" - ? buildSlackQuestionButtonActionId(state.buttonIndex + 1, choiceIndex) - : buildSlackReplyButtonActionId(state.buttonIndex + 1, choiceIndex), - text: { - type: "plain_text" as const, - text: truncateSlackText(button.label, SLACK_ACTION_LABEL_MAX), - emoji: true, - }, - ...(target.kind === "link" ? { url: target.url } : { value: target.value }), - ...(style ? { style } : {}), - }, - ]; - }) - .slice(0, SLACK_ACTION_BLOCK_ELEMENTS_MAX); - if (elements.length === 0) { - return state; - } - state.blocks.push({ - type: "actions", - block_id: `openclaw_reply_buttons_${++state.buttonIndex}`, - elements, - }); - return state; - } - const optionsLocal = block.options - .flatMap((option) => { - const target = resolveSlackOptionTarget(option); - return target ? [{ label: option.label, ...target }] : []; - }) - .filter(isRenderableSlackOption) - .slice(0, SLACK_STATIC_SELECT_OPTIONS_MAX); - const optionKinds = new Set(optionsLocal.map((option) => option.kind)); - if (optionsLocal.length === 0 || optionKinds.size !== 1) { - return state; - } - state.blocks.push({ - type: "actions", - block_id: `openclaw_reply_select_${++state.selectIndex}`, - elements: [ - { - type: "static_select", - action_id: - optionsLocal[0]?.kind === "approval" - ? buildSlackApprovalSelectActionId(state.selectIndex) - : optionsLocal[0]?.kind === "callback" - ? buildSlackCallbackSelectActionId(state.selectIndex) - : buildSlackReplySelectActionId(state.selectIndex), - placeholder: { - type: "plain_text", - text: truncateSlackText( - normalizeOptionalString(block.placeholder) ?? "Choose an option", - SLACK_ACTION_LABEL_MAX, - ), - emoji: true, - }, - options: optionsLocal.map((option, _choiceIndex) => ({ - text: { - type: "plain_text", - text: truncateSlackText(option.label, SLACK_ACTION_LABEL_MAX), - emoji: true, - }, - value: option.value, - })), - }, - ], - }); - return state; - }).blocks; + return buildSlackPresentationBlocks( + interactive ? legacyInteractiveReplyToPresentation(interactive) : undefined, + options, + ); } /** Render portable presentation blocks as Slack Block Kit blocks. */ diff --git a/extensions/slack/src/shared-interactive.test.ts b/extensions/slack/src/shared-interactive.test.ts index 192d466fb1f3..e58c24703ec0 100644 --- a/extensions/slack/src/shared-interactive.test.ts +++ b/extensions/slack/src/shared-interactive.test.ts @@ -339,6 +339,52 @@ describe("buildSlackInteractiveBlocks", () => { expect(buttonBlock.elements?.[2]?.style).toBe("primary"); expect(buttonBlock.elements?.[3]).not.toHaveProperty("style"); }); + + it.each([ + { + name: "approval", + action: { + type: "approval" as const, + approvalId: "request-1", + approvalKind: "exec" as const, + decision: "allow-once" as const, + }, + actionId: "openclaw:approval_button:5:1", + value: + 'openclaw:approval:v1:{"approvalId":"request-1","approvalKind":"exec","decision":"allow-once"}', + }, + { + name: "callback", + action: { type: "callback" as const, value: "plugin:opaque|value" }, + actionId: "openclaw:callback_button:5:1", + value: "plugin:opaque|value", + }, + { + name: "question", + action: { + type: "question" as const, + questionId: "ask_0123456789abcdef0123456789abcdef", + optionValue: "Production", + }, + actionId: "openclaw:question_button:5:1", + value: "slq1:ask_0123456789abcdef0123456789abcdef:0", + }, + ])( + "preserves typed $name authority through the legacy renderer", + ({ action, actionId, value }) => { + expect( + buildSlackInteractiveBlocks( + { blocks: [{ type: "buttons", buttons: [{ label: "Continue", action }] }] }, + { buttonIndexOffset: 4 }, + ), + ).toMatchObject([ + { + block_id: "openclaw_reply_buttons_5", + elements: [{ action_id: actionId, value }], + }, + ]); + }, + ); }); describe("buildSlackPresentationBlocks", () => {