diff --git a/extensions/slack/src/monitor/message-handler/dispatch-progress.ts b/extensions/slack/src/monitor/message-handler/dispatch-progress.ts index b5c0b94b2e93..9e01dd8afdff 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch-progress.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch-progress.ts @@ -322,7 +322,7 @@ export function createSlackProgressRuntime(runtimeParams: { mode: slackStreaming.mode, active: progressDraftActive, seed: progressSeed, - formatLine: escapeSlackMrkdwn, + formatLine: formatSlackProgressDraftLine, reasoningLinePrefix: "🧠 ", commentaryLinePrefix: "šŸ’¬ ", reasoningGate: previewToolProgressEnabled, @@ -442,7 +442,7 @@ export function createSlackProgressRuntime(runtimeParams: { entry: account.config, lines: [...progressDraft.getSnapshot().lines], seed: progressSeed, - formatLine: escapeSlackMrkdwn, + formatLine: formatSlackProgressDraftLine, narration: explanation, plan: steps, }); @@ -656,3 +656,7 @@ export function createSlackProgressRuntime(runtimeParams: { shouldYieldDraftProgress: () => shouldYieldDraftProgress(), }; } + +function formatSlackProgressDraftLine(line: string): string { + return /^(?:🧠|šŸ’¬)\s/u.test(line) ? line : escapeSlackMrkdwn(line); +} diff --git a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts index 47690f2ca986..9eabec7ab189 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts @@ -1966,7 +1966,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { ); expect(draftStream.update).toHaveBeenLastCalledWith( - ["Shelling", "", "• exec", "🧠 \\_Reading the Slack handler\\_"].join("\n"), + ["Shelling", "", "• exec", "🧠 _Reading the Slack handler_"].join("\n"), ); const updates = draftStream.update.mock.calls.map((call) => String(call[0])); expect(updates.join("\n")).not.toContain("Reasoning"); @@ -1995,7 +1995,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { ); expect(draftStream.update).toHaveBeenLastCalledWith( - ["Shelling", "", "• exec", "🧠 \\_Reading Checking\\_"].join("\n"), + ["Shelling", "", "• exec", "🧠 _Reading Checking_"].join("\n"), ); const updates = draftStream.update.mock.calls.map((call) => String(call[0])); expect(updates.join("\n")).not.toContain("Checking Reading"); @@ -2022,7 +2022,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { ); expect(draftStream.update).toHaveBeenLastCalledWith( - ["Shelling", "", "🧠 \\_Reading Checking\\_"].join("\n"), + ["Shelling", "", "🧠 _Reading Checking_"].join("\n"), ); const updates = draftStream.update.mock.calls.map((call) => String(call[0])); expect(updates.join("\n")).toContain("Reading Checking"); @@ -2049,7 +2049,7 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { ); expect(draftStream.update).toHaveBeenLastCalledWith( - ["Shelling", "", "🧠 \\_Thinking about Slack preview state\\_"].join("\n"), + ["Shelling", "", "🧠 _Thinking about Slack preview state_"].join("\n"), ); }); @@ -3749,6 +3749,37 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { expect(draftStream.update).toHaveBeenCalledTimes(updateCount); }); + it("preserves Markdown in Slack commentary drafts for the outbound renderer", async () => { + const draftStream = createDraftStreamStub(); + createSlackDraftStreamMock.mockReturnValueOnce(draftStream); + mockedSlackStreamingMode = "progress"; + mockedSlackDraftMode = "status_final"; + mockedDispatchSequence = []; + mockedReplyOptionEvents = [ + { + kind: "item", + itemKind: "preamble", + itemId: "preamble-1", + progressText: "I’m using the `monorepo` skill on Linux x86_64.", + }, + ]; + + await dispatchPreparedSlackMessage( + createPreparedSlackMessage({ + accountConfig: { + streaming: { + mode: "progress", + progress: { label: false, commentary: true, toolProgress: false }, + }, + }, + }), + ); + + expect(draftStream.update).toHaveBeenLastCalledWith( + "šŸ’¬ I’m using the `monorepo` skill on Linux x86_64.", + ); + }); + it("uses the enterprise event client for Slack commentary drafts", async () => { const draftStream = createDraftStreamStub(); createSlackDraftStreamMock.mockReturnValueOnce(draftStream); diff --git a/extensions/slack/src/progress-blocks.test.ts b/extensions/slack/src/progress-blocks.test.ts index aa4a33671c83..02f9e82b0770 100644 --- a/extensions/slack/src/progress-blocks.test.ts +++ b/extensions/slack/src/progress-blocks.test.ts @@ -200,6 +200,32 @@ describe("buildSlackProgressDraftBlocks", () => { ).toEqual([legacyLineBlock("• *Preamble*", "—"), legacyLineBlock("šŸ› ļø *Exec*", "run tests")]); }); + it("renders authored commentary Markdown in legacy rich draft details", () => { + expect( + buildSlackProgressDraftBlocks({ + lines: [ + { + id: "commentary:preamble-1", + kind: "item", + label: "Commentary", + text: "šŸ’¬ Rendering the `sample-widget` fixture on **example.test**.", + prefix: false, + }, + { + id: "reasoning", + kind: "item", + label: "Reasoning", + text: "_Reading the Slack handler_", + prefix: false, + }, + ], + }), + ).toEqual([ + legacyLineBlock("• *Commentary*", "Rendering the `sample-widget` fixture on *example.test*."), + legacyLineBlock("• *Reasoning*", "_Reading the Slack handler_"), + ]); + }); + it("does not emit legacy rich draft blocks when there are no lines or heading", () => { expect( buildSlackProgressDraftBlocks({ diff --git a/extensions/slack/src/progress-blocks.ts b/extensions/slack/src/progress-blocks.ts index 33d1432ac9a8..4123e664c9a3 100644 --- a/extensions/slack/src/progress-blocks.ts +++ b/extensions/slack/src/progress-blocks.ts @@ -8,6 +8,7 @@ import { formatPlanChecklistLines, } from "openclaw/plugin-sdk/channel-outbound"; import { SLACK_MAX_BLOCKS } from "./blocks-input.js"; +import { normalizeSlackOutboundText } from "./format.js"; import { escapeSlackMrkdwn } from "./monitor/mrkdwn.js"; import { truncateSlackText } from "./truncate.js"; @@ -77,9 +78,20 @@ function legacyLineTitle(line: ChannelProgressDraftLine): string { return `${line.icon ?? "•"} *${escapeSlackMrkdwn(line.label)}*`; } +function isAuthoredProgressLine(line: ChannelProgressDraftLine): boolean { + return line.id === "reasoning" || line.id?.startsWith("commentary:") === true; +} + function legacyLineDetail(line: ChannelProgressDraftLine, maxChars: number): string { const detail = lineDetailParts(line).join(" Ā· "); - return detail ? escapeSlackMrkdwn(compactDetail(detail, maxChars)) : "—"; + if (detail) { + return escapeSlackMrkdwn(compactDetail(detail, maxChars)); + } + if (isAuthoredProgressLine(line)) { + const text = line.text.replace(/^(?:🧠|šŸ’¬)\s+/u, ""); + return normalizeSlackOutboundText(compactDetail(text, maxChars)); + } + return "—"; } function lineTaskTitle(line: ChannelProgressDraftLine, maxLineChars: number): string {