fix(slack): Fix bad escaping in slack tool commentary (#119373)

* fix(slack): preserve Markdown in progress drafts

* fix(slack): retain escaping for tool progress

* fix(slack): scope progress line escaping

* fix(slack): preserve progress formatting by source

* test(slack): assert plan refresh rendering

* test(slack): keep progress regression coverage scoped

* update tests

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
This commit is contained in:
Sarah Fortune
2026-08-04 18:10:25 -07:00
committed by GitHub
parent 618fba92bf
commit c56eaca14b
4 changed files with 80 additions and 7 deletions
@@ -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);
}
@@ -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: "Im 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(
"💬 Im 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);
@@ -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({
+13 -1
View File
@@ -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 {