diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index 034edd698c74..396954435891 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -2374,7 +2374,7 @@ describe("dispatchTelegramMessage draft streaming", () => { }); expect(answerDraftStream.update).toHaveBeenCalledWith( - "Cracking\n\n`šŸ› ļø Exec`\n`šŸ› ļø git rev-parse --abbrev-ref HEAD`", + "Cracking\n\n`šŸ› ļø Exec`\n\n`šŸ› ļø git rev-parse --abbrev-ref HEAD`", ); expect(answerDraftStream.update).not.toHaveBeenCalledWith("Branch is up to date"); expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1); @@ -2434,7 +2434,7 @@ describe("dispatchTelegramMessage draft streaming", () => { expect.stringContaining("stdout line one"), ); expect(answerDraftStream.update).toHaveBeenLastCalledWith( - "Shelling\n\n`šŸ› ļø Exec`\n`šŸ”Ž Web Search: docs lookup`", + "Shelling\n\n`šŸ› ļø Exec`\n\n`šŸ”Ž Web Search: docs lookup`", ); expect(deliverReplies).not.toHaveBeenCalled(); }); @@ -2670,7 +2670,7 @@ describe("dispatchTelegramMessage draft streaming", () => { }); expect(createTelegramDraftStream).toHaveBeenCalledTimes(1); - expect(draftStream.update).toHaveBeenCalledWith("Shelling\n\n`šŸ› ļø Exec`\n• _Checking files_"); + expect(draftStream.update).toHaveBeenCalledWith("Shelling\n\n`šŸ› ļø Exec`\n\n• _Checking files_"); }); it("renders configured Telegram commentary progress from preamble item events", async () => { @@ -2840,7 +2840,7 @@ describe("dispatchTelegramMessage draft streaming", () => { }); expect(draftStream.update).toHaveBeenCalledWith( - "Shelling\n\n`šŸ”Ž Web Search: docs lookup`\n• `tests passed`", + "Shelling\n\n`šŸ”Ž Web Search: docs lookup`\n\n• `tests passed`", ); expect(draftStream.forceNewMessage).toHaveBeenCalledTimes(1); expect(draftStream.materialize).not.toHaveBeenCalled(); diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index 22b1ad7527f9..e95da3e01275 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -967,6 +967,10 @@ export const dispatchTelegramMessage = async ({ active: Boolean(answerLane.stream), seed: progressSeed, formatLine: formatTelegramProgressLine, + // Telegram's rich-markdown renderer collapses a lone "\n" to a space, so + // tool-progress lines need a blank line between them to stay on their own + // line (it renders "\n\n" as a single break, not a double). + lineSeparator: "\n\n", update: async (streamText, options) => { await prepareAnswerLaneForToolProgress(); answerLane.lastPartialText = streamText; diff --git a/src/channels/progress-draft-compositor.ts b/src/channels/progress-draft-compositor.ts index cf9188a3a1cb..20ab58bb18b3 100644 --- a/src/channels/progress-draft-compositor.ts +++ b/src/channels/progress-draft-compositor.ts @@ -39,6 +39,9 @@ export function createChannelProgressDraftCompositor(params: { deleteCurrent?: () => Promise | void; tryNativeUpdate?: (text: string) => Promise | boolean; formatLine?: (line: string) => string; + /** Separator between rendered draft lines; forwarded to the draft formatter. + * Telegram passes "\n\n" because its renderer collapses a lone newline. */ + lineSeparator?: string; isEmptyLine?: (line: ProgressDraftLine | undefined) => boolean; shouldStartNow?: (line: ProgressDraftLine | undefined) => boolean; }) { @@ -66,6 +69,7 @@ export function createChannelProgressDraftCompositor(params: { lines: draftLines, seed: params.seed, formatLine: options?.formatted === false ? undefined : params.formatLine, + lineSeparator: params.lineSeparator, }); const clearProgressState = (suppressed: boolean) => { diff --git a/src/channels/streaming.ts b/src/channels/streaming.ts index 0eb1971d34d8..c6473a78a026 100644 --- a/src/channels/streaming.ts +++ b/src/channels/streaming.ts @@ -1047,6 +1047,11 @@ export function formatChannelProgressDraftText(params: { formatLine?: (line: string) => string; /** Prefix used for plain progress lines that lack their own icon. */ bullet?: string; + /** Separator between rendered progress lines. Defaults to a single newline. + * Telegram's rich-markdown parser treats a lone "\n" as a soft break (space), + * so its draft passes "\n\n"; channels where a single newline already breaks + * the line (Discord) keep the default. */ + lineSeparator?: string; }): string { const rawLabel = resolveChannelProgressDraftLabel({ entry: params.entry, @@ -1058,6 +1063,7 @@ export function formatChannelProgressDraftText(params: { const maxLineChars = resolveChannelProgressDraftMaxLineChars(params.entry); const formatLine = params.formatLine ?? ((line: string) => line); const bullet = params.bullet ?? "•"; + const lineSeparator = params.lineSeparator ?? "\n"; const rawLines: Array = resolvedLabel ? [{ draftLabel: resolvedLabel }, ...params.lines] : params.lines; @@ -1090,7 +1096,7 @@ export function formatChannelProgressDraftText(params: { }); const renderedLines = lines.map((line) => line.text).filter((line) => Boolean(line)); if (renderedLines.length > 1 && lines[0]?.isLabelLine) { - return `${renderedLines[0]}\n\n${renderedLines.slice(1).join("\n")}`; + return `${renderedLines[0]}\n\n${renderedLines.slice(1).join(lineSeparator)}`; } - return renderedLines.join("\n"); + return renderedLines.join(lineSeparator); }