Skip to content

Commit 42dcf70

Browse files
authored
Fix Telegram rich progress detail updates (#93698)
Merged via squash. Prepared head SHA: 77da499 Co-authored-by: obviyus <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
1 parent 0278b59 commit 42dcf70

6 files changed

Lines changed: 71 additions & 10 deletions

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2626,7 +2626,11 @@ describe("dispatchTelegramMessage draft streaming", () => {
26262626

26272627
const lastUpdate = answerDraftStream.updatePreview.mock.calls.at(-1)?.[0];
26282628
expect(lastUpdate?.text).toContain("completed");
2629-
expect(lastUpdate?.text).not.toContain("install dependencies");
2629+
expect(lastUpdate?.text).toContain("install dependencies");
2630+
expect(lastUpdate?.richMessage).toEqual({
2631+
html: "<b>Shelling</b><br><b>🛠️ Exec</b> <code>install dependencies</code> <i>completed</i>",
2632+
skip_entity_detection: true,
2633+
});
26302634
});
26312635

26322636
it("sends trailing verbose status after a progress-mode final answer", async () => {

extensions/telegram/src/draft-stream.test-helpers.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Telegram helper module supports draft stream helpers behavior.
22
import { vi } from "vitest";
3+
import type { TelegramDraftPreview } from "./draft-stream.js";
34

45
type TestDraftStream = {
56
update: ReturnType<typeof vi.fn<(text: string) => void>>;
6-
updatePreview: ReturnType<typeof vi.fn<(preview: { text: string }) => void>>;
7+
updatePreview: ReturnType<typeof vi.fn<(preview: TelegramDraftPreview) => void>>;
78
flush: ReturnType<typeof vi.fn<() => Promise<void>>>;
89
messageId: ReturnType<typeof vi.fn<() => number | undefined>>;
910
visibleSinceMs: ReturnType<typeof vi.fn<() => number | undefined>>;
@@ -41,7 +42,7 @@ export function createTestDraftStream(params?: {
4142
lastDeliveredText = text.trimEnd();
4243
params?.onUpdate?.(text);
4344
}),
44-
updatePreview: vi.fn().mockImplementation((preview: { text: string }) => {
45+
updatePreview: vi.fn().mockImplementation((preview: TelegramDraftPreview) => {
4546
if (stopped) {
4647
return;
4748
}
@@ -95,7 +96,7 @@ export function createSequencedTestDraftStream(startMessageId = 1001): TestDraft
9596
previewRevision += 1;
9697
lastDeliveredText = text.trimEnd();
9798
}),
98-
updatePreview: vi.fn().mockImplementation((preview: { text: string }) => {
99+
updatePreview: vi.fn().mockImplementation((preview: TelegramDraftPreview) => {
99100
if (activeMessageId == null) {
100101
activeMessageId = nextMessageId++;
101102
visibleSinceMs = Date.now();

extensions/telegram/src/draft-stream.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,20 @@ describe("createTelegramDraftStream", () => {
632632
expect(api.editMessageText).not.toHaveBeenCalled();
633633
});
634634

635+
it("keeps rich preview html out of plain preview gating", async () => {
636+
const api = createMockDraftApi();
637+
const stream = createDraftStream(api, { richMessages: true, minInitialChars: 10 });
638+
639+
stream.updatePreview({
640+
text: "Plan",
641+
richMessage: { html: "<h2>Plan</h2><table><tr><td>A</td></tr></table>" },
642+
});
643+
await stream.flush();
644+
645+
expect(api.raw.sendRichMessage).not.toHaveBeenCalled();
646+
expect(api.sendMessage).not.toHaveBeenCalled();
647+
});
648+
635649
it("clamps rich previews to the block limit", async () => {
636650
const api = createMockDraftApi();
637651
const text = Array.from({ length: 501 }, (_, index) => `paragraph ${index}`).join("\n\n");

extensions/telegram/src/draft-stream.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ function telegramDraftRichPayloadLength(preview: TelegramDraftPreview): number {
135135
return richMessage.html?.length ?? richMessage.markdown?.length ?? 0;
136136
}
137137

138+
function resolveTelegramDraftRenderedText(
139+
preview: TelegramDraftPreview,
140+
richMessages: boolean,
141+
): string {
142+
return richMessages ? preview.text : normalizeTelegramDraftTransportPreview(preview).text;
143+
}
144+
138145
function findTelegramDraftChunkLength(
139146
text: string,
140147
maxChars: number,
@@ -147,7 +154,7 @@ function findTelegramDraftChunkLength(
147154
while (low <= high) {
148155
const mid = Math.floor((low + high) / 2);
149156
const preview = renderTelegramDraftPreview(text.slice(0, mid), renderText);
150-
const renderedText = normalizeTelegramDraftTransportPreview(preview).text.trimEnd();
157+
const renderedText = resolveTelegramDraftRenderedText(preview, richMessages).trimEnd();
151158
const payloadLength = richMessages
152159
? telegramDraftRichPayloadLength(preview)
153160
: renderedText.length;
@@ -254,7 +261,6 @@ export function createTelegramDraftStream(params: {
254261
preview,
255262
sendGeneration,
256263
}: PreviewSendParams): Promise<boolean> => {
257-
const transportPreview = normalizeTelegramDraftTransportPreview(preview);
258264
if (typeof streamMessageId === "number") {
259265
streamVisibleSinceMs ??= Date.now();
260266
if (richMessages) {
@@ -265,6 +271,7 @@ export function createTelegramDraftStream(params: {
265271
});
266272
return true;
267273
}
274+
const transportPreview = normalizeTelegramDraftTransportPreview(preview);
268275
if (transportPreview.parseMode === "HTML") {
269276
try {
270277
await params.api.editMessageText(chatId, streamMessageId, transportPreview.text, {
@@ -340,8 +347,7 @@ export function createTelegramDraftStream(params: {
340347
deliveredTextOffset === 0 && lastRequestedPreview?.text === trimmed
341348
? lastRequestedPreview
342349
: renderTelegramDraftPreview(currentText, params.renderText);
343-
const transportPreview = normalizeTelegramDraftTransportPreview(rendered);
344-
const renderedText = transportPreview.text.trimEnd();
350+
const renderedText = resolveTelegramDraftRenderedText(rendered, richMessages).trimEnd();
345351
const renderedPayloadLength = richMessages
346352
? telegramDraftRichPayloadLength(rendered)
347353
: renderedText.length;

src/channels/streaming.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,11 +1111,13 @@ export function mergeChannelProgressDraftLine<TLine extends string | ChannelProg
11111111
resolveProgressDraftLineMergeKeys(entry).some((entryKey) => lineKeys.includes(entryKey)),
11121112
);
11131113
if (existingIndex >= 0) {
1114-
if (normalizeChannelProgressDraftLineIdentity(lines[existingIndex]) === normalized) {
1114+
const replacement = mergeProgressDraftLineUpdate(lines[existingIndex], line);
1115+
const replacementIdentity = normalizeChannelProgressDraftLineIdentity(replacement);
1116+
if (normalizeChannelProgressDraftLineIdentity(lines[existingIndex]) === replacementIdentity) {
11151117
return lines;
11161118
}
11171119
const next = [...lines];
1118-
next[existingIndex] = line;
1120+
next[existingIndex] = replacement;
11191121
return next.slice(-maxLines);
11201122
}
11211123
}
@@ -1126,6 +1128,36 @@ export function mergeChannelProgressDraftLine<TLine extends string | ChannelProg
11261128
return [...lines, line].slice(-maxLines);
11271129
}
11281130

1131+
function mergeProgressDraftLineUpdate<TLine extends string | ChannelProgressDraftLine>(
1132+
previous: TLine,
1133+
line: TLine,
1134+
): TLine {
1135+
if (typeof previous !== "object" || typeof line !== "object") {
1136+
return line;
1137+
}
1138+
if (
1139+
line.kind !== "command-output" ||
1140+
!line.status ||
1141+
(line.detail && line.detail !== line.status)
1142+
) {
1143+
return line;
1144+
}
1145+
const previousDetail = previous.detail?.trim();
1146+
if (!previousDetail || previousDetail === previous.status) {
1147+
return line;
1148+
}
1149+
const replacement = {
1150+
...line,
1151+
detail: previousDetail,
1152+
};
1153+
replacement.text = getProgressDraftLineText(replacement);
1154+
setProgressDraftLineCorrelationKey(
1155+
replacement,
1156+
progressDraftLineCorrelationKeys.get(line) ?? progressDraftLineCorrelationKeys.get(previous),
1157+
);
1158+
return replacement;
1159+
}
1160+
11291161
function resolveProgressDraftLineMergeKeys(line: string | ChannelProgressDraftLine): string[] {
11301162
if (typeof line !== "object") {
11311163
return [];

src/plugin-sdk/channel-streaming.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,8 @@ describe("channel-streaming", () => {
602602
id: "tool:call-1-output",
603603
kind: "command-output",
604604
status: "completed",
605+
detail: "install dependencies",
606+
text: "🛠️ completed; install dependencies",
605607
});
606608

607609
const recoveredItemLine = buildChannelProgressDraftLine({
@@ -633,6 +635,8 @@ describe("channel-streaming", () => {
633635
id: "command-2",
634636
kind: "command-output",
635637
status: "completed",
638+
detail: "install dependencies failed",
639+
text: "🛠️ completed; install dependencies failed",
636640
},
637641
]);
638642
});

0 commit comments

Comments
 (0)