fix(telegram): preserve pre-tool text during CLI tool calls

Preserve Claude CLI commentary outside Telegram’s disposable progress stream so tool-summary collapse cannot erase user-visible pre-tool text.

Validated with real-user Telegram E2E and focused regression coverage.
This commit is contained in:
Ayaan Zaidi
2026-08-03 08:21:56 +09:00
committed by GitHub
parent 6f01e514fe
commit 5aae66f4e3
3 changed files with 39 additions and 0 deletions
@@ -336,10 +336,17 @@ export function createTelegramReplyDelivery(params: {
!params.draft.isAnswerToolProgressOnly() &&
!ownedByQueuedRotation &&
segment.update.text.trimEnd() === params.draft.answerLane.lastPartialText.trimEnd();
const isDurableProgressCommentary =
params.streamMode === "progress" &&
info.kind === "block" &&
effectivePayload.isCommentary === true;
// CLI finals exclude separately classified commentary. Send that block outside
// the disposable progress stream or its collapse summary erases the text.
const suppressProgressAnswerBlock =
params.streamMode === "progress" &&
info.kind === "block" &&
segment.lane === "answer" &&
!isDurableProgressCommentary &&
!reply.hasMedia &&
!hasExecApprovalPayload(effectivePayload) &&
telegramButtons === undefined;
@@ -381,6 +388,7 @@ export function createTelegramReplyDelivery(params: {
payload: lanePayload,
infoKind: info.kind,
buttons: telegramButtons,
allowStream: !isDurableProgressCommentary,
});
if (
segment.lane === "answer" &&
@@ -241,6 +241,7 @@ export async function runTelegramDispatchTurn(params: {
? params.progress.commentaryProgressEnabled
: undefined,
progressPreambleEnabled: params.progress.progressPreambleEnabled,
commentaryPayloadsEnabled: params.progress.progressPreambleEnabled,
reasoningPayloadsEnabled: params.draft.durableReasoningPayloadsEnabled,
onToolStart: params.progress.handleToolStart,
onItemEvent: params.progress.handleItemEvent,
@@ -314,6 +314,36 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => {
);
});
it("keeps Claude CLI pre-tool commentary after the progress window collapses", async () => {
const markers = "Test markers: caribou-lampion-473, fromage-quantique, satellite-en-tricot";
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
async ({ dispatcherOptions, replyOptions }) => {
expect(replyOptions?.commentaryPayloadsEnabled).toBe(true);
await replyOptions?.onItemEvent?.({
kind: "preamble",
itemId: "commentary-1",
progressText: markers,
suppressDurableProgress: true,
});
await replyOptions?.onBlockReplyQueued?.({ text: markers, isCommentary: true });
await dispatcherOptions.deliver({ text: markers, isCommentary: true }, { kind: "block" });
await replyOptions?.onToolStart?.({ name: "Bash", phase: "start" });
await dispatcherOptions.deliver({ text: "TEST DONE" }, { kind: "final" });
return { queuedFinal: true };
},
);
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress" } },
});
expectWindowCollapsedTo(answerDraftStream, "🛠️ 1 tool call · ⏱️ 1s");
expect(allDeliveredReplyTexts()).toEqual([markers, "TEST DONE"]);
});
it("never streams an interim answer block into the progress window (Discord parity)", async () => {
// Progress mode: the window is a pure activity log. An intermediate assistant
// answer block (info.kind === "block", before the final) must NOT render into