Skip to content

Commit a8b5f5d

Browse files
committed
fix(telegram): send progress previews as html text
1 parent bbe9669 commit a8b5f5d

2 files changed

Lines changed: 62 additions & 81 deletions

File tree

extensions/telegram/src/bot-message-dispatch.test.ts

Lines changed: 51 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,10 @@ describe("dispatchTelegramMessage draft streaming", () => {
398398
return expectRecordFields(mockCallArg(dispatchReplyWithBufferedBlockDispatcher), expected);
399399
}
400400

401+
function telegramHtmlPreview(html: string) {
402+
return { text: html, parseMode: "HTML" as const };
403+
}
404+
401405
function createContext(overrides?: Partial<TelegramMessageContext>): TelegramMessageContext {
402406
const base = {
403407
ctxPayload: {},
@@ -2435,7 +2439,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
24352439
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Site A shows X.");
24362440
expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Site A shows X.");
24372441
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
2438-
expect.objectContaining({ text: expect.stringMatching(/`🛠 Exec`$/) }),
2442+
expect.objectContaining({ text: expect.stringMatching(/<b>🛠 Exec<\/b>$/) }),
24392443
);
24402444
expect(answerDraftStream.update).toHaveBeenNthCalledWith(3, "Final answer");
24412445
expect(answerDraftStream.clear).toHaveBeenCalledTimes(1);
@@ -2462,7 +2466,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
24622466

24632467
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Site A shows X.");
24642468
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
2465-
expect.objectContaining({ text: expect.stringMatching(/`🛠 Exec`$/) }),
2469+
expect.objectContaining({ text: expect.stringMatching(/<b>🛠 Exec<\/b>$/) }),
24662470
);
24672471
expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Site B shows Y.");
24682472
expect(answerDraftStream.update).toHaveBeenNthCalledWith(3, "Final answer");
@@ -2504,7 +2508,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
25042508
await dispatchWithContext({ context: createContext() });
25052509

25062510
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
2507-
expect.objectContaining({ text: expect.stringMatching(/`🛠 Exec`$/) }),
2511+
expect.objectContaining({ text: expect.stringMatching(/<b>🛠 Exec<\/b>$/) }),
25082512
);
25092513
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Branch is up to date");
25102514
expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1);
@@ -2530,7 +2534,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
25302534
await dispatchWithContext({ context: createContext() });
25312535

25322536
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
2533-
expect.objectContaining({ text: expect.stringMatching(/`🛠 Exec`$/) }),
2537+
expect.objectContaining({ text: expect.stringMatching(/<b>🛠 Exec<\/b>$/) }),
25342538
);
25352539
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Branch is up to date");
25362540
expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1);
@@ -2584,13 +2588,11 @@ describe("dispatchTelegramMessage draft streaming", () => {
25842588
telegramCfg: { streaming: { mode: "progress" } },
25852589
});
25862590

2587-
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith({
2588-
text: "Cracking\n\n`🛠️ Exec`\n`🛠️ git rev-parse --abbrev-ref HEAD`",
2589-
richMessage: {
2590-
html: "<b>Cracking</b><br><b>🛠️ Exec</b><br><b>🛠️ Exec</b> <code>git rev-parse --abbrev-ref HEAD</code>",
2591-
skip_entity_detection: true,
2592-
},
2593-
});
2591+
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
2592+
telegramHtmlPreview(
2593+
"<b>Cracking</b><br><b>🛠️ Exec</b><br><b>🛠️ Exec</b> <code>git rev-parse --abbrev-ref HEAD</code>",
2594+
),
2595+
);
25942596
expect(answerDraftStream.update).not.toHaveBeenCalledWith("Branch is up to date");
25952597
expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1);
25962598
expect(answerDraftStream.clear).toHaveBeenCalledTimes(1);
@@ -2627,10 +2629,9 @@ describe("dispatchTelegramMessage draft streaming", () => {
26272629
const lastUpdate = answerDraftStream.updatePreview.mock.calls.at(-1)?.[0];
26282630
expect(lastUpdate?.text).toContain("install dependencies");
26292631
expect(lastUpdate?.text).not.toContain("completed");
2630-
expect(lastUpdate?.richMessage).toEqual({
2631-
html: "<b>Shelling</b><br><b>🛠️ Exec</b> <code>install dependencies</code>",
2632-
skip_entity_detection: true,
2633-
});
2632+
expect(lastUpdate).toEqual(
2633+
telegramHtmlPreview("<b>Shelling</b><br><b>🛠️ Exec</b> <code>install dependencies</code>"),
2634+
);
26342635
});
26352636

26362637
it("sends trailing verbose status after a progress-mode final answer", async () => {
@@ -2650,13 +2651,9 @@ describe("dispatchTelegramMessage draft streaming", () => {
26502651
telegramCfg: { streaming: { mode: "progress" } },
26512652
});
26522653

2653-
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith({
2654-
text: "Cracking\n\n`🛠️ Exec`",
2655-
richMessage: {
2656-
html: "<b>Cracking</b><br><b>🛠️ Exec</b>",
2657-
skip_entity_detection: true,
2658-
},
2659-
});
2654+
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
2655+
telegramHtmlPreview("<b>Cracking</b><br><b>🛠️ Exec</b>"),
2656+
);
26602657
expect(answerDraftStream.update).toHaveBeenCalledTimes(1);
26612658
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, trailingFinalStatusText);
26622659
expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(2);
@@ -2691,7 +2688,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
26912688
);
26922689
expect(answerDraftStream.updatePreview).toHaveBeenLastCalledWith(
26932690
expect.objectContaining({
2694-
text: "Shelling\n\n`🛠️ Exec`\n`🔎 Web Search: docs lookup`",
2691+
text: "<b>Shelling</b><br><b>🛠️ Exec</b><br><b>🔎 Web Search</b> <code>docs lookup</code>",
26952692
}),
26962693
);
26972694
expect(deliverReplies).not.toHaveBeenCalled();
@@ -2716,7 +2713,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
27162713

27172714
expect(answerDraftStream.updatePreview).toHaveBeenCalledTimes(1);
27182715
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
2719-
expect.objectContaining({ text: "Shelling\n\n`🛠️ Exec`" }),
2716+
telegramHtmlPreview("<b>Shelling</b><br><b>🛠️ Exec</b>"),
27202717
);
27212718
expectDeliveredReply(0, { text: "Branch is up to date" });
27222719
});
@@ -2746,7 +2743,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
27462743

27472744
expect(answerDraftStream.updatePreview).toHaveBeenCalledTimes(1);
27482745
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
2749-
expect.objectContaining({ text: "Shelling\n\n`🛠️ Exec`" }),
2746+
telegramHtmlPreview("<b>Shelling</b><br><b>🛠️ Exec</b>"),
27502747
);
27512748
expectDeliveredReply(0, { text: "Branch is up to date" });
27522749
});
@@ -2780,7 +2777,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
27802777

27812778
expect(answerDraftStream.updatePreview).toHaveBeenCalledTimes(1);
27822779
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
2783-
expect.objectContaining({ text: "Shelling\n\n`🛠️ Exec`" }),
2780+
telegramHtmlPreview("<b>Shelling</b><br><b>🛠️ Exec</b>"),
27842781
);
27852782
expectDeliveredReply(0, { text: "Branch is up to date" });
27862783
});
@@ -2929,13 +2926,9 @@ describe("dispatchTelegramMessage draft streaming", () => {
29292926
telegramCfg: { streaming: { mode: "progress", progress: { label: "Shelling" } } },
29302927
});
29312928

2932-
expect(draftStream.updatePreview).toHaveBeenCalledWith({
2933-
text: "Shelling\n\n`🛠️ Exec`",
2934-
richMessage: {
2935-
html: "<b>Shelling</b><br><b>🛠️ Exec</b>",
2936-
skip_entity_detection: true,
2937-
},
2938-
});
2929+
expect(draftStream.updatePreview).toHaveBeenCalledWith(
2930+
telegramHtmlPreview("<b>Shelling</b><br><b>🛠️ Exec</b>"),
2931+
);
29392932
expect(draftStream.flush).toHaveBeenCalled();
29402933
});
29412934

@@ -2973,13 +2966,11 @@ describe("dispatchTelegramMessage draft streaming", () => {
29732966
},
29742967
});
29752968

2976-
expect(draftStream.updatePreview).toHaveBeenLastCalledWith({
2977-
text: "Shelling\n\n`🛠️ exit 2; command false`",
2978-
richMessage: {
2979-
html: "<b>Shelling</b><br><b>🛠️ Exec</b> <code>command false</code> <i>exit 2</i>",
2980-
skip_entity_detection: true,
2981-
},
2982-
});
2969+
expect(draftStream.updatePreview).toHaveBeenLastCalledWith(
2970+
telegramHtmlPreview(
2971+
"<b>Shelling</b><br><b>🛠️ Exec</b> <code>command false</code> <i>exit 2</i>",
2972+
),
2973+
);
29832974
});
29842975

29852976
it("hides command titles in Telegram status-only progress draft previews", async () => {
@@ -3016,13 +3007,9 @@ describe("dispatchTelegramMessage draft streaming", () => {
30163007
},
30173008
});
30183009

3019-
expect(draftStream.updatePreview).toHaveBeenLastCalledWith({
3020-
text: "Shelling\n\n`🛠️ exit 2`",
3021-
richMessage: {
3022-
html: "<b>Shelling</b><br><b>🛠️ Exec</b> <code>exit 2</code>",
3023-
skip_entity_detection: true,
3024-
},
3025-
});
3010+
expect(draftStream.updatePreview).toHaveBeenLastCalledWith(
3011+
telegramHtmlPreview("<b>Shelling</b><br><b>🛠️ Exec</b> <code>exit 2</code>"),
3012+
);
30263013
});
30273014

30283015
it("composes streamed reasoning with tool progress in Telegram progress drafts", async () => {
@@ -3043,13 +3030,9 @@ describe("dispatchTelegramMessage draft streaming", () => {
30433030
});
30443031

30453032
expect(createTelegramDraftStream).toHaveBeenCalledTimes(1);
3046-
expect(draftStream.updatePreview).toHaveBeenCalledWith({
3047-
text: "Shelling\n\n`🛠️ Exec`\n• _Checking files_",
3048-
richMessage: {
3049-
html: "<b>Shelling</b><br><b>🛠️ Exec</b><br><i>Checking files</i>",
3050-
skip_entity_detection: true,
3051-
},
3052-
});
3033+
expect(draftStream.updatePreview).toHaveBeenCalledWith(
3034+
telegramHtmlPreview("<b>Shelling</b><br><b>🛠️ Exec</b><br><i>Checking files</i>"),
3035+
);
30533036
});
30543037

30553038
it("renders configured Telegram commentary progress from preamble item events", async () => {
@@ -3076,13 +3059,9 @@ describe("dispatchTelegramMessage draft streaming", () => {
30763059
},
30773060
});
30783061

3079-
expect(draftStream.updatePreview).toHaveBeenCalledWith({
3080-
text: "Shelling\n\n_Checking recent context_",
3081-
richMessage: {
3082-
html: "<b>Shelling</b><br><i>Checking recent context</i>",
3083-
skip_entity_detection: true,
3084-
},
3085-
});
3062+
expect(draftStream.updatePreview).toHaveBeenCalledWith(
3063+
telegramHtmlPreview("<b>Shelling</b><br><i>Checking recent context</i>"),
3064+
);
30863065
});
30873066

30883067
it("suppresses Telegram preamble progress when commentary is disabled", async () => {
@@ -3137,10 +3116,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
31373116
},
31383117
});
31393118

3140-
expect(draftStream.updatePreview).toHaveBeenCalledWith({
3141-
text: "Shelling",
3142-
richMessage: { html: "<b>Shelling</b>", skip_entity_detection: true },
3143-
});
3119+
expect(draftStream.updatePreview).toHaveBeenCalledWith(telegramHtmlPreview("<b>Shelling</b>"));
31443120
expect(draftStream.flush).toHaveBeenCalled();
31453121
});
31463122

@@ -3170,18 +3146,16 @@ describe("dispatchTelegramMessage draft streaming", () => {
31703146
});
31713147

31723148
await vi.waitFor(() =>
3173-
expect(draftStream.updatePreview).toHaveBeenCalledWith(
3174-
expect.objectContaining({ text: "Working" }),
3175-
),
3149+
expect(draftStream.updatePreview).toHaveBeenCalledWith(telegramHtmlPreview("<b>Working</b>")),
31763150
);
31773151
expect(draftStream.updatePreview).not.toHaveBeenCalledWith(
3178-
expect.objectContaining({ text: "Working." }),
3152+
telegramHtmlPreview("<b>Working.</b>"),
31793153
);
31803154
expect(draftStream.updatePreview).not.toHaveBeenCalledWith(
3181-
expect.objectContaining({ text: "Working.." }),
3155+
telegramHtmlPreview("<b>Working..</b>"),
31823156
);
31833157
expect(draftStream.updatePreview).not.toHaveBeenCalledWith(
3184-
expect.objectContaining({ text: "Working..." }),
3158+
telegramHtmlPreview("<b>Working...</b>"),
31853159
);
31863160
finishRun?.();
31873161
await run;
@@ -3205,7 +3179,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
32053179
const updateBeforeStatusReaction = draftStream.updatePreview.mock.calls.at(-1)?.[0]?.text;
32063180
releaseSetTool?.();
32073181
await pendingToolStart;
3208-
expect(updateBeforeStatusReaction).toBe("Shelling\n\n`🛠️ Exec`");
3182+
expect(updateBeforeStatusReaction).toBe("<b>Shelling</b><br><b>🛠️ Exec</b>");
32093183
return { queuedFinal: false };
32103184
});
32113185

@@ -3241,13 +3215,11 @@ describe("dispatchTelegramMessage draft streaming", () => {
32413215
telegramCfg: { streaming: { mode: "progress", progress: { label: "Shelling" } } },
32423216
});
32433217

3244-
expect(draftStream.updatePreview).toHaveBeenCalledWith({
3245-
text: "Shelling\n\n`🔎 Web Search: docs lookup`\n• `tests passed`",
3246-
richMessage: {
3247-
html: "<b>Shelling</b><br><b>🔎 Web Search</b> <code>docs lookup</code><br><b>Update</b> <code>tests passed</code>",
3248-
skip_entity_detection: true,
3249-
},
3250-
});
3218+
expect(draftStream.updatePreview).toHaveBeenCalledWith(
3219+
telegramHtmlPreview(
3220+
"<b>Shelling</b><br><b>🔎 Web Search</b> <code>docs lookup</code><br><b>Update</b> <code>tests passed</code>",
3221+
),
3222+
);
32513223
expect(draftStream.forceNewMessage).toHaveBeenCalledTimes(1);
32523224
expect(draftStream.materialize).not.toHaveBeenCalled();
32533225
expect(draftStream.clear).toHaveBeenCalledTimes(1);

extensions/telegram/src/bot-message-dispatch.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,16 +466,21 @@ function renderTelegramProgressLine(line: ChannelProgressDraftCompositorLine): s
466466
function renderTelegramProgressDraftPreview(
467467
text: string,
468468
lines: readonly ChannelProgressDraftCompositorLine[],
469+
richMessages: boolean,
469470
): TelegramDraftPreview {
470471
const trimmed = text.trimEnd();
471472
const [heading] = trimmed.split(/\r?\n/u, 1);
472473
const renderedLines = lines.map(renderTelegramProgressLine).filter(Boolean);
473474
const htmlParts = heading?.trim()
474475
? [`<b>${escapeTelegramProgressHtml(heading.trim())}</b>`, ...renderedLines]
475476
: renderedLines;
477+
const html = htmlParts.join("<br>");
478+
if (!richMessages) {
479+
return { text: html, parseMode: "HTML" };
480+
}
476481
return {
477482
text: trimmed,
478-
richMessage: buildTelegramRichHtml(htmlParts.join("<br>"), { skipEntityDetection: true }),
483+
richMessage: buildTelegramRichHtml(html, { skipEntityDetection: true }),
479484
};
480485
}
481486

@@ -1074,7 +1079,11 @@ export const dispatchTelegramMessage = async ({
10741079
answerLane.hasStreamedMessage = true;
10751080
answerLane.finalized = false;
10761081
answerLane.stream?.updatePreview(
1077-
renderTelegramProgressDraftPreview(streamText, options?.lines ?? []),
1082+
renderTelegramProgressDraftPreview(
1083+
streamText,
1084+
options?.lines ?? [],
1085+
telegramCfg.richMessages === true,
1086+
),
10781087
);
10791088
if (options?.flush) {
10801089
await answerLane.stream?.flush();

0 commit comments

Comments
 (0)