fix(telegram): keep streamed tool-progress lines on separate lines

Telegram's rich-markdown renderer treats a lone "\n" as a soft break
(rendered as a space), so streamed tool-progress draft lines joined by a
single newline collapsed onto one line. Pass "\n\n" as the progress-draft
line separator for Telegram; it renders a blank line as a single break, so
each tool/thinking/commentary line gets its own line again. Other channels
keep the single-newline default, so Discord and the rest are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Peter Lindsey
2026-06-15 10:11:48 +08:00
committed by Ayaan Zaidi
parent 50c82b3020
commit c847db550f
4 changed files with 20 additions and 6 deletions
@@ -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();
@@ -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;
@@ -39,6 +39,9 @@ export function createChannelProgressDraftCompositor(params: {
deleteCurrent?: () => Promise<void> | void;
tryNativeUpdate?: (text: string) => Promise<boolean> | 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) => {
+8 -2
View File
@@ -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<string | ChannelProgressDraftLine | { draftLabel: string }> = 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);
}