fix: defer non-text block emission until message completes

Tool call blocks (and thinking) arrive in streaming updates before their
args are populated — emitting immediately gave `Tool call bash {}`.

Buffer non-text blocks during onMessageUpdate; flush them in
onMessageStart (previous message now complete, args fully populated)
and onAgentEnd (single-message turns that never trigger onMessageStart).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wassname
2026-04-22 16:15:31 +08:00
parent 29ac175e79
commit 64f9d0242f
+18 -14
View File
@@ -403,7 +403,7 @@ export default function (pi: ExtensionAPI) {
let setupInProgress = false; let setupInProgress = false;
let previewState: TelegramPreviewState | undefined; let previewState: TelegramPreviewState | undefined;
let displayMode: DisplayMode = "compact"; let displayMode: DisplayMode = "compact";
let emittedNonTextBlockCount = 0; let pendingNonTextBlocks: TelegramAssistantDisplayBlock[] = [];
let draftSupport: "unknown" | "supported" | "unsupported" = "unknown"; let draftSupport: "unknown" | "supported" | "unsupported" = "unknown";
let nextDraftId = 0; let nextDraftId = 0;
let currentTelegramModel: Model<any> | undefined; let currentTelegramModel: Model<any> | undefined;
@@ -2042,7 +2042,7 @@ export default function (pi: ExtensionAPI) {
} }
if (startPlan.activeTurn) { if (startPlan.activeTurn) {
activeTelegramTurn = { ...startPlan.activeTurn }; activeTelegramTurn = { ...startPlan.activeTurn };
emittedNonTextBlockCount = 0; pendingNonTextBlocks = [];
previewState = createPreviewState(); previewState = createPreviewState();
startTypingLoop(ctx); startTypingLoop(ctx);
} }
@@ -2074,7 +2074,12 @@ export default function (pi: ExtensionAPI) {
await finalizePreview(activeTelegramTurn.chatId); await finalizePreview(activeTelegramTurn.chatId);
} }
} }
emittedNonTextBlockCount = 0; // Flush non-text blocks from the completed previous message now that args are fully populated
for (const block of pendingNonTextBlocks) {
const msg = renderBlockMessage(block, displayMode);
if (msg) void sendMarkdownReply(activeTelegramTurn.chatId, activeTelegramTurn.replyToMessageId, msg);
}
pendingNonTextBlocks = [];
previewState = createPreviewState(); previewState = createPreviewState();
}, },
onMessageUpdate: async (event, _ctx) => { onMessageUpdate: async (event, _ctx) => {
@@ -2083,17 +2088,10 @@ export default function (pi: ExtensionAPI) {
if (!previewState) previewState = createPreviewState(); if (!previewState) previewState = createPreviewState();
const allBlocks = getMessageBlocks(nextEvent.message); const allBlocks = getMessageBlocks(nextEvent.message);
const nonTextBlocks = allBlocks.filter((b) => b.type !== "text");
// Emit each new non-text block as its own Telegram message // Buffer non-text blocks; emit after the message completes (in onMessageStart / onAgentEnd)
for (let i = emittedNonTextBlockCount; i < nonTextBlocks.length; i++) { // so tool_call blocks have their args fully populated before being sent
const block = nonTextBlocks[i]!; pendingNonTextBlocks = allBlocks.filter((b) => b.type !== "text");
const msg = renderBlockMessage(block, displayMode);
if (msg) {
void sendMarkdownReply(activeTelegramTurn.chatId, activeTelegramTurn.replyToMessageId, msg);
}
emittedNonTextBlockCount++;
}
// Stream text content in the preview message // Stream text content in the preview message
const textContent = allBlocks const textContent = allBlocks
@@ -2111,7 +2109,6 @@ export default function (pi: ExtensionAPI) {
currentAbort = undefined; currentAbort = undefined;
stopTypingLoop(); stopTypingLoop();
activeTelegramTurn = undefined; activeTelegramTurn = undefined;
emittedNonTextBlockCount = 0;
activeTelegramToolExecutions = 0; activeTelegramToolExecutions = 0;
pendingTelegramModelSwitch = undefined; pendingTelegramModelSwitch = undefined;
telegramTurnDispatchPending = false; telegramTurnDispatchPending = false;
@@ -2145,6 +2142,13 @@ export default function (pi: ExtensionAPI) {
return; return;
} }
// Flush any non-text blocks from the final message (single-message turns never trigger onMessageStart)
for (const block of pendingNonTextBlocks) {
const msg = renderBlockMessage(block, displayMode);
if (msg) void sendMarkdownReply(turn.chatId, turn.replyToMessageId, msg);
}
pendingNonTextBlocks = [];
// Finalize the streaming text preview (only for normal completions, not abort/empty) // Finalize the streaming text preview (only for normal completions, not abort/empty)
if (endPlan.kind === "text") { if (endPlan.kind === "text") {
const finalText = previewState?.pendingText.trim() || assistant.text?.trim(); const finalText = previewState?.pendingText.trim() || assistant.text?.trim();