Skip to content

Commit 36c047c

Browse files
committed
fix(channels): unify progress draft line formatting
1 parent df5c453 commit 36c047c

15 files changed

Lines changed: 526 additions & 73 deletions

File tree

docs/concepts/progress-drafts.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,16 @@ progress chatter for that turn.
5151

5252
A progress draft has two parts:
5353

54-
| Part | Purpose |
55-
| -------------- | ----------------------------------------------------------------- |
56-
| Label | A short title such as `Thinking...` or `Shelling...`. |
57-
| Progress lines | Compact run updates such as tool calls, task steps, or approvals. |
54+
| Part | Purpose |
55+
| -------------- | --------------------------------------------------------------------------- |
56+
| Label | A short title such as `Thinking...` or `Shelling...`. |
57+
| Progress lines | Compact run updates using the same tool labels and icons as verbose output. |
5858

5959
The label appears after the agent starts meaningful work and either remains busy
6060
for five seconds or emits a second work event. Plain text-only replies do not
6161
show a progress draft. Progress lines are added only when the agent emits useful
62-
work updates. The final answer replaces the draft when possible; otherwise
62+
work updates, for example `🛠️ Exec`, `🔎 Web Search`, or `✍️ Write: to /tmp/file`.
63+
The final answer replaces the draft when possible; otherwise
6364
OpenClaw sends the final answer normally and cleans up or stops updating the
6465
draft according to the channel's transport.
6566

extensions/discord/src/monitor/message-handler.process.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,7 +1524,7 @@ describe("processDiscordMessage draft streaming", () => {
15241524

15251525
await runProcessDiscordMessage(ctx);
15261526

1527-
expect(draftStream.update).toHaveBeenCalledWith("Shelling\n• tool: exec\n• exec done");
1527+
expect(draftStream.update).toHaveBeenCalledWith("Shelling\n🛠️ Exec\n• exec done");
15281528
expect(deliverDiscordReply).not.toHaveBeenCalled();
15291529
expect(editMessageDiscord).toHaveBeenCalledWith(
15301530
"c1",
@@ -1557,7 +1557,7 @@ describe("processDiscordMessage draft streaming", () => {
15571557

15581558
await runProcessDiscordMessage(ctx);
15591559

1560-
expect(draftStream.update).toHaveBeenCalledWith("Shelling\n• tool: first\n• tool: second");
1560+
expect(draftStream.update).toHaveBeenCalledWith("Shelling\n🧩 First\n🧩 Second");
15611561
expect(draftStream.forceNewMessage).not.toHaveBeenCalled();
15621562
});
15631563

extensions/discord/src/monitor/message-handler.process.ts

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import {
1111
createChannelReplyPipeline,
1212
resolveChannelSourceReplyDeliveryMode,
1313
} from "openclaw/plugin-sdk/channel-reply-pipeline";
14-
import { resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-streaming";
14+
import {
15+
formatChannelProgressDraftLine,
16+
resolveChannelStreamingBlockEnabled,
17+
} from "openclaw/plugin-sdk/channel-streaming";
1518
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
1619
import {
1720
hasFinalInboundReplyDispatch,
@@ -665,47 +668,89 @@ export async function processDiscordMessage(
665668
await maybeBindStatusReactionsToToolReaction(payload);
666669
await statusReactions.setTool(payload.name);
667670
await draftPreview.pushToolProgress(
668-
payload.name ? `tool: ${payload.name}` : "tool running",
671+
formatChannelProgressDraftLine({
672+
event: "tool",
673+
name: payload.name,
674+
phase: payload.phase,
675+
args: payload.args,
676+
}),
669677
{ toolName: payload.name },
670678
);
671679
},
672680
onItemEvent: async (payload) => {
673681
await draftPreview.pushToolProgress(
674-
payload.progressText ?? payload.summary ?? payload.title ?? payload.name,
682+
formatChannelProgressDraftLine({
683+
event: "item",
684+
itemKind: payload.kind,
685+
title: payload.title,
686+
name: payload.name,
687+
phase: payload.phase,
688+
status: payload.status,
689+
summary: payload.summary,
690+
progressText: payload.progressText,
691+
meta: payload.meta,
692+
}),
675693
);
676694
},
677695
onPlanUpdate: async (payload) => {
678696
if (payload.phase !== "update") {
679697
return;
680698
}
681699
await draftPreview.pushToolProgress(
682-
payload.explanation ?? payload.steps?.[0] ?? "planning",
700+
formatChannelProgressDraftLine({
701+
event: "plan",
702+
phase: payload.phase,
703+
title: payload.title,
704+
explanation: payload.explanation,
705+
steps: payload.steps,
706+
}),
683707
);
684708
},
685709
onApprovalEvent: async (payload) => {
686710
if (payload.phase !== "requested") {
687711
return;
688712
}
689713
await draftPreview.pushToolProgress(
690-
payload.command ? `approval: ${payload.command}` : "approval requested",
714+
formatChannelProgressDraftLine({
715+
event: "approval",
716+
phase: payload.phase,
717+
title: payload.title,
718+
command: payload.command,
719+
reason: payload.reason,
720+
message: payload.message,
721+
}),
691722
);
692723
},
693724
onCommandOutput: async (payload) => {
694725
if (payload.phase !== "end") {
695726
return;
696727
}
697728
await draftPreview.pushToolProgress(
698-
payload.name
699-
? `${payload.name}${payload.exitCode === 0 ? " ✓" : payload.exitCode != null ? ` (exit ${payload.exitCode})` : ""}`
700-
: payload.title,
729+
formatChannelProgressDraftLine({
730+
event: "command-output",
731+
phase: payload.phase,
732+
title: payload.title,
733+
name: payload.name,
734+
status: payload.status,
735+
exitCode: payload.exitCode,
736+
}),
701737
);
702738
},
703739
onPatchSummary: async (payload) => {
704740
if (payload.phase !== "end") {
705741
return;
706742
}
707743
await draftPreview.pushToolProgress(
708-
payload.summary ?? payload.title ?? "patch applied",
744+
formatChannelProgressDraftLine({
745+
event: "patch",
746+
phase: payload.phase,
747+
title: payload.title,
748+
name: payload.name,
749+
added: payload.added,
750+
modified: payload.modified,
751+
deleted: payload.deleted,
752+
summary: payload.summary,
753+
}),
709754
);
710755
},
711756
onCompactionStart: async () => {

extensions/matrix/src/matrix/monitor/handler.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2705,7 +2705,7 @@ describe("matrix monitor handler draft streaming", () => {
27052705
await vi.waitFor(() => {
27062706
expect(sendSingleTextMessageMatrixMock).toHaveBeenCalledTimes(1);
27072707
});
2708-
expect(sendSingleTextMessageMatrixMock.mock.calls[0]?.[1]).toMatch(/\n- `tool: read_file`$/);
2708+
expect(sendSingleTextMessageMatrixMock.mock.calls[0]?.[1]).toMatch(/\n`🧩 Read File`$/);
27092709

27102710
await deliver({ text: "Done" }, { kind: "final" });
27112711

extensions/matrix/src/matrix/monitor/handler.ts

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
createChannelProgressDraftGate,
3+
formatChannelProgressDraftLine,
34
formatChannelProgressDraftText,
45
isChannelProgressDraftWorkToolName,
56
resolveChannelProgressDraftMaxLines,
@@ -376,23 +377,6 @@ function formatMatrixToolProgressMarkdownCode(text: string): string {
376377
return `\`${safe}\``;
377378
}
378379

379-
function formatMatrixCommandOutputToolProgress(payload: {
380-
exitCode?: number | null;
381-
name?: string;
382-
title?: string;
383-
}) {
384-
if (!payload.name) {
385-
return payload.title;
386-
}
387-
if (payload.exitCode === 0) {
388-
return `${payload.name} ok`;
389-
}
390-
if (payload.exitCode != null) {
391-
return `${payload.name} (exit ${payload.exitCode})`;
392-
}
393-
return payload.name;
394-
}
395-
396380
export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
397381
const {
398382
client,
@@ -1595,40 +1579,91 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
15951579
...options,
15961580
onToolStart: async (payload) => {
15971581
const toolName = payload.name?.trim();
1598-
await pushPreviewToolProgress(toolName ? `tool: ${toolName}` : "tool running", {
1599-
toolName,
1600-
});
1582+
await pushPreviewToolProgress(
1583+
formatChannelProgressDraftLine({
1584+
event: "tool",
1585+
name: toolName,
1586+
phase: payload.phase,
1587+
args: payload.args,
1588+
}),
1589+
{ toolName },
1590+
);
16011591
},
16021592
onItemEvent: async (payload) => {
16031593
await pushPreviewToolProgress(
1604-
payload.progressText ?? payload.summary ?? payload.title ?? payload.name,
1594+
formatChannelProgressDraftLine({
1595+
event: "item",
1596+
itemKind: payload.kind,
1597+
title: payload.title,
1598+
name: payload.name,
1599+
phase: payload.phase,
1600+
status: payload.status,
1601+
summary: payload.summary,
1602+
progressText: payload.progressText,
1603+
meta: payload.meta,
1604+
}),
16051605
);
16061606
},
16071607
onPlanUpdate: async (payload) => {
16081608
if (payload.phase !== "update") {
16091609
return;
16101610
}
1611-
await pushPreviewToolProgress(payload.explanation ?? payload.steps?.[0] ?? "planning");
1611+
await pushPreviewToolProgress(
1612+
formatChannelProgressDraftLine({
1613+
event: "plan",
1614+
phase: payload.phase,
1615+
title: payload.title,
1616+
explanation: payload.explanation,
1617+
steps: payload.steps,
1618+
}),
1619+
);
16121620
},
16131621
onApprovalEvent: async (payload) => {
16141622
if (payload.phase !== "requested") {
16151623
return;
16161624
}
16171625
await pushPreviewToolProgress(
1618-
payload.command ? `approval: ${payload.command}` : "approval requested",
1626+
formatChannelProgressDraftLine({
1627+
event: "approval",
1628+
phase: payload.phase,
1629+
title: payload.title,
1630+
command: payload.command,
1631+
reason: payload.reason,
1632+
message: payload.message,
1633+
}),
16191634
);
16201635
},
16211636
onCommandOutput: async (payload) => {
16221637
if (payload.phase !== "end") {
16231638
return;
16241639
}
1625-
await pushPreviewToolProgress(formatMatrixCommandOutputToolProgress(payload));
1640+
await pushPreviewToolProgress(
1641+
formatChannelProgressDraftLine({
1642+
event: "command-output",
1643+
phase: payload.phase,
1644+
title: payload.title,
1645+
name: payload.name,
1646+
status: payload.status,
1647+
exitCode: payload.exitCode,
1648+
}),
1649+
);
16261650
},
16271651
onPatchSummary: async (payload) => {
16281652
if (payload.phase !== "end") {
16291653
return;
16301654
}
1631-
await pushPreviewToolProgress(payload.summary ?? payload.title ?? "patch applied");
1655+
await pushPreviewToolProgress(
1656+
formatChannelProgressDraftLine({
1657+
event: "patch",
1658+
phase: payload.phase,
1659+
title: payload.title,
1660+
name: payload.name,
1661+
added: payload.added,
1662+
modified: payload.modified,
1663+
deleted: payload.deleted,
1664+
summary: payload.summary,
1665+
}),
1666+
);
16321667
},
16331668
};
16341669
};

extensions/msteams/src/reply-dispatcher.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ describe("createMSTeamsReplyDispatcher", () => {
351351
await dispatcher.replyOptions.onToolStart?.({ name: "exec" });
352352

353353
expect(streamInstances[0]?.sendInformativeUpdate).toHaveBeenCalledWith(
354-
"Working\n- tool: web_search\n- tool: exec",
354+
"Working\n🔎 Web Search\n🛠️ Exec",
355355
);
356356
});
357357

0 commit comments

Comments
 (0)