diff --git a/CHANGELOG.md b/CHANGELOG.md index da52a46ae7d..5d24d467d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai - WhatsApp: stage `qrcode` through root mirrored runtime dependencies so packaged QR pairing can render from staged plugin-runtime-deps installs. Fixes #75394. Thanks @FelipeX2001. - Discord/voice: apply per-channel Discord `systemPrompt` overrides to voice transcript turns by forwarding the trusted channel prompt through the voice agent run. Fixes #47095. Thanks @qearlyao. - Discord/native commands: send component-only interaction replies from slash command and status handlers instead of treating renderable Discord components as an empty response. Thanks @vincentkoc. +- Slack/slash commands: send block-only slash command replies instead of dropping Slack block payloads with no plain-text fallback. Thanks @vincentkoc. - Gateway/agent: reject strict `openclaw agent --deliver` requests with missing delivery targets before starting the agent run, so users do not wait for a completed turn that cannot send anywhere. Thanks @vincentkoc. - Setup/import: honor non-interactive `--import-from` onboarding flags by running the migration import path instead of silently completing normal setup without importing anything. Thanks @vincentkoc. - Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram. diff --git a/extensions/slack/src/monitor/replies.test.ts b/extensions/slack/src/monitor/replies.test.ts index 4ad6f25fe59..2c745752eaf 100644 --- a/extensions/slack/src/monitor/replies.test.ts +++ b/extensions/slack/src/monitor/replies.test.ts @@ -297,4 +297,31 @@ describe("deliverSlackSlashReplies chunking", () => { response_type: "ephemeral", }); }); + + it("sends block-only slash replies instead of dropping them", async () => { + const respond = vi.fn(async () => undefined); + const blocks = [{ type: "divider" }]; + + await deliverSlackSlashReplies({ + replies: [ + { + channelData: { + slack: { + blocks, + }, + }, + }, + ], + respond, + ephemeral: false, + textLimit: 8000, + }); + + expect(respond).toHaveBeenCalledTimes(1); + expect(respond).toHaveBeenCalledWith({ + text: "", + blocks, + response_type: "in_channel", + }); + }); }); diff --git a/extensions/slack/src/monitor/replies.ts b/extensions/slack/src/monitor/replies.ts index cf0428782f4..77212c43446 100644 --- a/extensions/slack/src/monitor/replies.ts +++ b/extensions/slack/src/monitor/replies.ts @@ -116,6 +116,7 @@ export async function deliverReplies(params: { export type SlackRespondFn = (payload: { text: string; + blocks?: ReturnType; response_type?: "ephemeral" | "in_channel"; }) => Promise; @@ -202,14 +203,19 @@ export async function deliverSlackSlashReplies(params: { tableMode?: MarkdownTableMode; chunkMode?: ChunkMode; }) { - const messages: string[] = []; + const messages: Array<{ text: string; blocks?: ReturnType }> = []; const chunkLimit = Math.min(params.textLimit, SLACK_TEXT_LIMIT); for (const payload of params.replies) { const reply = resolveSendableOutboundReplyParts(payload); + const slackBlocks = readSlackReplyBlocks(payload); const text = reply.hasText && !isSilentReplyText(reply.trimmedText, SILENT_REPLY_TOKEN) ? reply.trimmedText : undefined; + if (slackBlocks?.length && !reply.hasMedia) { + messages.push({ text: text ?? "", blocks: slackBlocks }); + continue; + } const combined = [text ?? "", ...reply.mediaUrls].filter(Boolean).join("\n"); if (!combined) { continue; @@ -226,7 +232,7 @@ export async function deliverSlackSlashReplies(params: { chunks.push(combined); } for (const chunk of chunks) { - messages.push(chunk); + messages.push({ text: chunk }); } } @@ -236,7 +242,7 @@ export async function deliverSlackSlashReplies(params: { // Slack slash command responses can be multi-part by sending follow-ups via response_url. const responseType = params.ephemeral ? "ephemeral" : "in_channel"; - for (const text of messages) { - await params.respond({ text, response_type: responseType }); + for (const message of messages) { + await params.respond({ ...message, response_type: responseType }); } }