fix(channels): preserve command progress detail (#94868)

Merged via squash.

Prepared head SHA: 3217f45e61
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
Vincent Koc
2026-06-19 16:36:36 +08:00
committed by GitHub
parent e6743eb783
commit 5e329f4065
7 changed files with 40 additions and 35 deletions
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -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: "<b>Shelling</b><br><b>🛠️ Exec</b> <code>completed</code>",
html: "<b>Shelling</b><br><b>🛠️ Exec</b> <code>install dependencies</code>",
skip_entity_detection: true,
});
});
+3 -3
View File
@@ -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", () => {
+14 -11
View File
@@ -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<ChannelProgressDraftLineInput, { event: "item" }>,
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<TLine extends string | ChannelProgressDraf
if (
line.kind !== "command-output" ||
!line.status ||
line.status === "completed" ||
(line.detail && line.detail !== line.status)
) {
return line;
}
const previousDetail = previous.detail?.trim();
if (!previousDetail || previousDetail === previous.status) {
if (
!previousDetail ||
previousDetail === previous.status ||
isTerminalProgressStatus(previous.status)
) {
return line;
}
const replacement = {
+11 -10
View File
@@ -601,16 +601,16 @@ describe("channel-streaming", () => {
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 () => {