From 5e329f40656a901c8a48ab837e739fe9990e9b2e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 19 Jun 2026 16:36:36 +0800 Subject: [PATCH] fix(channels): preserve command progress detail (#94868) Merged via squash. Prepared head SHA: 3217f45e61fe63cb3dbc30ab82c076d0085d5072 Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com> Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com> Reviewed-by: @vincentkoc --- .../monitor/message-handler.process.test.ts | 4 +-- .../matrix/src/matrix/monitor/handler.test.ts | 9 ++++--- .../msteams/src/reply-dispatcher.test.ts | 4 +-- .../telegram/src/bot-message-dispatch.test.ts | 6 ++--- src/channels/streaming.test.ts | 6 ++--- src/channels/streaming.ts | 25 +++++++++++-------- src/plugin-sdk/channel-streaming.test.ts | 21 ++++++++-------- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/extensions/discord/src/monitor/message-handler.process.test.ts b/extensions/discord/src/monitor/message-handler.process.test.ts index 09473c9bc56a..9bd5fbbd5ec5 100644 --- a/extensions/discord/src/monitor/message-handler.process.test.ts +++ b/extensions/discord/src/monitor/message-handler.process.test.ts @@ -3057,8 +3057,8 @@ describe("processDiscordMessage draft streaming", () => { await runProcessDiscordMessage(ctx); const lastUpdate = draftStream.update.mock.calls.at(-1)?.[0]; - expect(lastUpdate).toContain("completed"); - expect(lastUpdate).not.toContain("install dependencies"); + expect(lastUpdate).toContain("install dependencies"); + expect(lastUpdate).not.toContain("completed"); }); it("drops later tool warning finals after progress preview final replies", async () => { diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 7bb8a20a6810..d4eebc4c4f94 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -3229,13 +3229,13 @@ describe("matrix monitor handler draft streaming", () => { expect(editMessageMatrixMock).toHaveBeenCalledWith( "!room:example.org", "$draft1", - expect.stringContaining("completed"), + expect.stringContaining("Exec"), expect.any(Object), ); const recoveredEdit = mockCalls(editMessageMatrixMock, "editMessageMatrix").find( - ([, eventId, body]) => - eventId === "$draft1" && typeof body === "string" && body.includes("completed"), + ([, eventId, body]) => eventId === "$draft1" && typeof body === "string", ); + expect(recoveredEdit?.[2]).not.toContain("completed"); expect(recoveredEdit?.[2]).not.toContain("failed"); expect(recoveredEdit?.[2]).not.toContain("run openclaw cron -> run jq"); }); @@ -3293,7 +3293,8 @@ describe("matrix monitor handler draft streaming", () => { ([, eventId, body]) => eventId === "$draft1" && typeof body === "string" && body.includes("completed"), ); - expect(completedEdit?.[2]).not.toContain("install dependencies"); + expect(completedEdit).toBeUndefined(); + expect(singleTextMessageBody()).toContain("install dependencies"); }); it("replaces Matrix patch progress when the patch summary completes", async () => { diff --git a/extensions/msteams/src/reply-dispatcher.test.ts b/extensions/msteams/src/reply-dispatcher.test.ts index f8dcea1f4f9e..3f6b19325b2b 100644 --- a/extensions/msteams/src/reply-dispatcher.test.ts +++ b/extensions/msteams/src/reply-dispatcher.test.ts @@ -454,8 +454,8 @@ describe("createMSTeamsReplyDispatcher", () => { }); const lastUpdate = getStreamMock().update.mock.calls.at(-1)?.[0]; - expect(lastUpdate).toContain("completed"); - expect(lastUpdate).not.toContain("install dependencies"); + expect(lastUpdate).toContain("install dependencies"); + expect(lastUpdate).not.toContain("completed"); }); it("replaces reasoning progress snapshots in progress mode", async () => { diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index 6eebb0e12524..57672686829b 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -2625,10 +2625,10 @@ describe("dispatchTelegramMessage draft streaming", () => { }); const lastUpdate = answerDraftStream.updatePreview.mock.calls.at(-1)?.[0]; - expect(lastUpdate?.text).toContain("completed"); - expect(lastUpdate?.text).not.toContain("install dependencies"); + expect(lastUpdate?.text).toContain("install dependencies"); + expect(lastUpdate?.text).not.toContain("completed"); expect(lastUpdate?.richMessage).toEqual({ - html: "Shelling
🛠️ Exec completed", + html: "Shelling
🛠️ Exec install dependencies", skip_entity_detection: true, }); }); diff --git a/src/channels/streaming.test.ts b/src/channels/streaming.test.ts index 47437bd10a13..10e67e373d06 100644 --- a/src/channels/streaming.test.ts +++ b/src/channels/streaming.test.ts @@ -24,7 +24,7 @@ describe("buildChannelProgressDraftLine", () => { }); }); - it("uses completed status when successful command output has no title", () => { + it("uses the tool label when successful command output has no title", () => { const line = buildChannelProgressDraftLine({ event: "command-output", phase: "end", @@ -34,10 +34,10 @@ describe("buildChannelProgressDraftLine", () => { expect(line).toMatchObject({ kind: "command-output", - text: "🛠️ completed", - detail: "completed", + text: "🛠️ Exec", status: "completed", }); + expect(line?.detail).toBeUndefined(); }); it("keeps command status and title in raw command progress lines", () => { diff --git a/src/channels/streaming.ts b/src/channels/streaming.ts index 41723768e0e3..179441805121 100644 --- a/src/channels/streaming.ts +++ b/src/channels/streaming.ts @@ -411,6 +411,15 @@ function resolveCommandProgressCorrelationKey(input: { toolCallId?: string }): s return toolCallId ? `command:${toolCallId}` : undefined; } +function isTerminalProgressStatus(status: string | undefined): boolean { + const normalized = normalizeOptionalLowercaseString(status); + return ( + normalized === "completed" || + normalized === "failed" || + normalized?.startsWith("exit ") === true + ); +} + function isEmptyReasoningProgressItem( input: Extract, meta: string | undefined, @@ -444,15 +453,6 @@ function buildCommandOutputProgressLine( return line; } if (status === "completed") { - if (!line.detail) { - const statusLine = { - ...line, - detail: status, - text: formatToolAggregate(name, [status], { markdown: options?.markdown }), - }; - setProgressDraftLineCorrelationKey(statusLine, correlationKey); - return statusLine; - } return line; } if (!line.detail || line.detail === status) { @@ -1153,13 +1153,16 @@ function mergeProgressDraftLineUpdate { expect(updated[0]).toMatchObject({ id: "tool:call-1-output", kind: "command-output", - detail: "completed", + detail: "install dependencies", status: "completed", - text: "🛠️ completed", + text: "🛠️ install dependencies", }); expect( formatChannelProgressDraftText({ lines: updated, entry: { streaming: { progress: { label: false } } }, }), - ).toBe("🛠️ completed"); + ).toBe("🛠️ install dependencies"); const recoveredItemLine = buildChannelProgressDraftLine({ event: "item", @@ -632,19 +632,20 @@ describe("channel-streaming", () => { if (!recoveredItemLine || !recoveredCommandLine) { throw new Error("expected recovered command progress lines"); } - expect( - mergeChannelProgressDraftLine([recoveredItemLine], recoveredCommandLine, { - maxLines: 4, - }), - ).toMatchObject([ + const recoveredUpdated = mergeChannelProgressDraftLine( + [recoveredItemLine], + recoveredCommandLine, + { maxLines: 4 }, + ); + expect(recoveredUpdated).toMatchObject([ { id: "command-2", kind: "command-output", - detail: "completed", status: "completed", - text: "🛠️ completed", + text: "🛠️ Bash", }, ]); + expect(recoveredUpdated[0]).not.toHaveProperty("detail"); }); it("starts progress drafts after five seconds or a second work event", async () => {