diff --git a/extensions/discord/src/monitor/message-handler.draft-preview.ts b/extensions/discord/src/monitor/message-handler.draft-preview.ts index fff107ebf31..f6a973a6e9f 100644 --- a/extensions/discord/src/monitor/message-handler.draft-preview.ts +++ b/extensions/discord/src/monitor/message-handler.draft-preview.ts @@ -103,6 +103,9 @@ export function createDiscordDraftPreviewController(params: { draftStream, previewToolProgressEnabled, suppressDefaultToolProgressMessages, + get isProgressMode() { + return discordStreamMode === "progress"; + }, get finalizedViaPreviewMessage() { return finalizedViaPreviewMessage; }, @@ -252,7 +255,12 @@ export function createDiscordDraftPreviewController(params: { }, }); }, - handleAssistantMessageBoundary: forceNewMessageIfNeeded, + handleAssistantMessageBoundary() { + if (discordStreamMode === "progress") { + return; + } + forceNewMessageIfNeeded(); + }, async flush() { if (!draftStream) { return; diff --git a/extensions/discord/src/monitor/message-handler.process.test.ts b/extensions/discord/src/monitor/message-handler.process.test.ts index ae377ef58a6..009364a06d0 100644 --- a/extensions/discord/src/monitor/message-handler.process.test.ts +++ b/extensions/discord/src/monitor/message-handler.process.test.ts @@ -1500,6 +1500,68 @@ describe("processDiscordMessage draft streaming", () => { expect(draftStream.flush).toHaveBeenCalledTimes(1); }); + it("keeps Discord progress drafts instead of delivering text-only interim blocks", async () => { + const draftStream = createMockDraftStreamForTest(); + + dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => { + await params?.dispatcher.sendBlockReply({ text: "on it" }); + await params?.replyOptions?.onToolStart?.({ name: "exec", phase: "start" }); + await params?.dispatcher.sendFinalReply({ text: "done" }); + return { queuedFinal: true, counts: { final: 1, tool: 0, block: 1 } }; + }); + + const ctx = await createAutomaticSourceDeliveryContext({ + discordConfig: { + streaming: { + mode: "progress", + progress: { + label: "Shelling", + }, + }, + }, + }); + + await runProcessDiscordMessage(ctx); + + expect(draftStream.update).toHaveBeenCalledWith("Shelling"); + expect(draftStream.update).toHaveBeenCalledWith("Shelling\n• tool: exec"); + expect(deliverDiscordReply).not.toHaveBeenCalled(); + expect(editMessageDiscord).toHaveBeenCalledWith( + "c1", + "preview-1", + { content: "done" }, + expect.objectContaining({ rest: expect.anything() }), + ); + }); + + it("keeps Discord progress lines across assistant boundaries", async () => { + const draftStream = createMockDraftStreamForTest(); + + dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => { + await params?.replyOptions?.onToolStart?.({ name: "first", phase: "start" }); + await params?.replyOptions?.onAssistantMessageStart?.(); + await params?.replyOptions?.onToolStart?.({ name: "second", phase: "start" }); + return createNoQueuedDispatchResult(); + }); + + const ctx = await createAutomaticSourceDeliveryContext({ + discordConfig: { + streaming: { + mode: "progress", + progress: { + label: "Shelling", + }, + }, + }, + }); + + await runProcessDiscordMessage(ctx); + + expect(draftStream.update).toHaveBeenCalledWith("Shelling\n• tool: first"); + expect(draftStream.update).toHaveBeenCalledWith("Shelling\n• tool: first\n• tool: second"); + expect(draftStream.forceNewMessage).not.toHaveBeenCalled(); + }); + it("keeps standalone Discord tool progress when partial preview lines are disabled", async () => { createMockDraftStreamForTest(); diff --git a/extensions/discord/src/monitor/message-handler.process.ts b/extensions/discord/src/monitor/message-handler.process.ts index 9050f8792be..25b68886335 100644 --- a/extensions/discord/src/monitor/message-handler.process.ts +++ b/extensions/discord/src/monitor/message-handler.process.ts @@ -430,6 +430,12 @@ export async function processDiscordMessage( return; } const draftStream = draftPreview.draftStream; + if (draftStream && draftPreview.isProgressMode && info.kind === "block") { + const reply = resolveSendableOutboundReplyParts(payload); + if (!reply.hasMedia && !payload.isError) { + return; + } + } if (draftStream && isFinal) { draftPreview.markFinalDeliveryHandled(); const reply = resolveSendableOutboundReplyParts(payload);