fix: use plain agent progress labels (#107260)

* fix(channels): simplify agent progress labels

* chore: keep changelog release-owned

* chore: refresh bundled channel metadata
This commit is contained in:
Peter Steinberger
2026-07-14 01:00:24 -07:00
committed by GitHub
parent 9da229c8ba
commit b940339c32
11 changed files with 39 additions and 121 deletions
@@ -1,2 +1,2 @@
33404fe321c4413d7d0d4bdd9697fe942ea17dbc6505761a68b528fc1d978437 plugin-sdk-api-baseline.json
c84e9d2181a2c8e2889984e180405846028def0dc347f489c333c0794f54a4c7 plugin-sdk-api-baseline.jsonl
0302d93c3f37910e833b21b33588d154793f3c2c9bcb4e3008cbec432afaaf27 plugin-sdk-api-baseline.json
a657e8e63db0f185d0917fc3660420dd0aa55f1a58544863b70bd6f36752f621 plugin-sdk-api-baseline.jsonl
+5 -7
View File
@@ -15,7 +15,7 @@ message once real work starts, edits it as the agent reads, plans, calls
tools, or waits for approval, then turns it into the final answer.
```text
Shelling...
Working...
📖 from docs/concepts/progress-drafts.md
🔎 Web Search: for "discord edit message"
🛠️ Bash: run tests
@@ -58,7 +58,7 @@ migration, see [Streaming and chunking](/concepts/streaming).
| Part | Purpose |
| --------------- | --------------------------------------------------------------------------------- |
| Status headline | On Discord and Telegram, the model preamble; Discord adds a utility filler. |
| Label | Optional starter/status line such as `Working` or `Shelling`. |
| Label | Optional starter/status line such as `Working`. |
| Progress lines | Compact run updates using the same tool icons and detail formatter as `/verbose`. |
For raw tool progress, the label appears once the agent starts meaningful work
@@ -94,14 +94,12 @@ block-reply delivery — use `streaming.block.enabled` for that.
## Configure labels
Progress labels live under `channels.<channel>.streaming.progress`. The default
raw tool-line label is `"auto"`, which picks from OpenClaw's built-in
single-word label pool. A status headline hides that implicit label; set
raw tool-line label is `"auto"`, which uses the plain built-in `Working`
label. A status headline hides that implicit label; set
`label: "auto"` explicitly if you want a label above it too:
```text
Working, Shelling, Scuttling, Clawing, Pinching, Molting, Bubbling, Tiding,
Reefing, Cracking, Sifting, Brining, Nautiling, Krilling, Barnacling,
Lobstering, Tidepooling, Pearling, Snapping, Surfacing
Working
```
Use a fixed label:
@@ -2318,7 +2318,7 @@ describe("processDiscordMessage draft streaming", () => {
await runProcessDiscordMessage(ctx);
const updates = draftStream.update.mock.calls.map((call) => call[0]);
expect(updates).toEqual(["Pinching\n\n🛠️ Exec\n• exec done"]);
expect(updates).toEqual(["Working\n\n🛠️ Exec\n• exec done"]);
expectFinalWithProgressReceipt("done", "🛠️ 1 tool call");
// The working draft deletes once the receipt-bearing final landed.
expect(editMessageDiscord).not.toHaveBeenCalled();
@@ -2647,7 +2647,7 @@ describe("processDiscordMessage draft streaming", () => {
await runProcessDiscordMessage(ctx);
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("message_tool_only");
expect(draftStream.update).toHaveBeenCalledWith("Pinching\n\n🛠️ Exec\n• exec done");
expect(draftStream.update).toHaveBeenCalledWith("Working\n\n🛠️ Exec\n• exec done");
expect(deliverDiscordReply).not.toHaveBeenCalled();
});
@@ -3116,7 +3116,7 @@ describe("processDiscordMessage draft streaming", () => {
expect(firstDispatchParams().replyOptions?.disableBlockStreaming).toBe(true);
});
it("keeps progress label visible when Discord tool progress lines are disabled", async () => {
it("uses the plain default progress label when Discord tool lines are disabled", async () => {
const elapseProgressDraftStartDelay = useProgressDraftStartDelay();
const draftStream = createMockDraftStreamForTest();
@@ -3133,7 +3133,6 @@ describe("processDiscordMessage draft streaming", () => {
streaming: {
mode: "progress",
progress: {
label: "Shelling",
toolProgress: false,
},
},
@@ -3143,7 +3142,7 @@ describe("processDiscordMessage draft streaming", () => {
await runProcessDiscordMessage(ctx);
expect(draftStream.update).toHaveBeenCalledTimes(1);
expect(draftStream.update).toHaveBeenCalledWith("Shelling");
expect(draftStream.update).toHaveBeenCalledWith("Working");
expect(draftStream.flush).toHaveBeenCalledTimes(1);
expect(
requireRecord(firstDispatchParams().replyOptions, "dispatch reply options")
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -97,7 +97,7 @@ function createChannelProgressUiHints(params: {
},
"streaming.progress.labels": {
label: `${channelLabel} Progress Label Pool`,
help: 'Candidate labels for streaming.progress.label="auto". Leave unset to use OpenClaw built-in progress labels.',
help: 'Candidate labels for streaming.progress.label="auto". Leave unset to use the built-in "Working" label.',
},
"streaming.progress.maxLines": {
label: `${channelLabel} Progress Max Lines`,
+1 -1
View File
@@ -237,7 +237,7 @@ describe("channel-streaming", () => {
});
it("uses auto progress labels when no explicit label is configured", () => {
expect(DEFAULT_PROGRESS_DRAFT_LABELS[0]).toBe("Working");
expect(DEFAULT_PROGRESS_DRAFT_LABELS).toEqual(["Working"]);
expect(resolveChannelProgressDraftLabel({ random: () => 0 })).toBe(
DEFAULT_PROGRESS_DRAFT_LABELS[0],
);
+1 -22
View File
@@ -1,25 +1,4 @@
export const DEFAULT_PROGRESS_DRAFT_LABELS = [
"Working",
"Shelling",
"Scuttling",
"Clawing",
"Pinching",
"Molting",
"Bubbling",
"Tiding",
"Reefing",
"Cracking",
"Sifting",
"Brining",
"Nautiling",
"Krilling",
"Barnacling",
"Lobstering",
"Tidepooling",
"Pearling",
"Snapping",
"Surfacing",
] as const;
export const DEFAULT_PROGRESS_DRAFT_LABELS = ["Working"] as const;
function hashProgressSeed(seed: string): number {
let hash = 2166136261;
+1 -11
View File
@@ -1,7 +1,6 @@
// Control UI E2E tests cover chat run lifecycle behavior through the Gateway WebSocket.
import { chromium, type Browser, type Page } from "playwright";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { DEFAULT_PROGRESS_DRAFT_LABELS } from "../../../src/shared/progress-labels.js";
import { CHAT_RUN_STATUS_TOAST_DURATION_MS } from "../pages/chat/run-lifecycle.ts";
import {
canRunPlaywrightChromium,
@@ -66,22 +65,13 @@ describeControlUiE2e("Control UI chat run lifecycle", () => {
await currentPage.getByRole("button", { name: "Send message" }).click();
await gateway.waitForRequest("chat.send");
await currentPage.locator(".chat-working-indicator").waitFor();
const progressLabelBefore = await currentPage
.locator(".chat-working-indicator__status span:last-child")
.textContent();
expect(DEFAULT_PROGRESS_DRAFT_LABELS.slice(1).map((label) => `${label}`)).toContain(
progressLabelBefore,
);
await currentPage.clock.runFor(177_000);
await expect
.poll(() => currentPage.locator(".chat-working-indicator__elapsed").textContent())
.toBe("2m 57s");
const progressLabelAfter = await currentPage
.locator(".chat-working-indicator__status span:last-child")
.textContent();
expect(progressLabelAfter).toBe(progressLabelBefore);
await currentPage.getByText("Working…", { exact: true }).waitFor();
});
it("clears shared session activity when chat final arrives first", async () => {
+1 -10
View File
@@ -3,7 +3,6 @@ import { copyFile, mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { chromium, type Browser, type BrowserContext } from "playwright";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { DEFAULT_PROGRESS_DRAFT_LABELS } from "../../../src/shared/progress-labels.js";
import {
canRunPlaywrightChromium,
installMockGateway,
@@ -110,19 +109,11 @@ describeMantisWebUiChat("Mantis Control UI web chat proof", () => {
await page.getByText("saved 875.3k tokens", { exact: true }).waitFor();
await page.locator(".chat-working-indicator").waitFor();
const progressLabel = await page
.locator(".chat-working-indicator__status span:last-child")
.textContent();
expect(DEFAULT_PROGRESS_DRAFT_LABELS.slice(1).map((label) => `${label}`)).toContain(
progressLabel,
);
await page.getByText("Working…", { exact: true }).waitFor();
await page.clock.runFor(177_000);
await expect
.poll(() => page.locator(".chat-working-indicator__elapsed").textContent())
.toBe("2m 57s");
expect(
await page.locator(".chat-working-indicator__status span:last-child").textContent(),
).toBe(progressLabel);
await page.screenshot({ fullPage: true, path: path.join(artifactDir, "web-ui-chat.png") });
await gateway.emitChatFinal({ runId: params.idempotencyKey ?? "", text: reply });
@@ -2,7 +2,6 @@
import { html, render } from "lit";
import { afterEach, describe, expect, it, vi } from "vitest";
import { i18n } from "../../../i18n/index.ts";
import type { MessageGroup } from "../../../lib/chat/chat-types.ts";
import { setUiTimeFormatPreference } from "../../../lib/format.ts";
import { renderMessageGroup, renderStreamGroup } from "./chat-message.ts";
@@ -453,8 +452,7 @@ function mediaTicketPayload(mediaTicket: string, ttlMs = 5 * 60 * 1000) {
};
}
afterEach(async () => {
await i18n.setLocale("en");
afterEach(() => {
markdownRenderMock.mockClear();
document.querySelectorAll("[data-delete-confirm-fixture]").forEach((element) => {
element.remove();
@@ -1049,7 +1047,7 @@ describe("grouped chat rendering", () => {
expect(container.querySelector(".chat-reading-indicator")).not.toBeNull();
expect(container.querySelector(".chat-working-indicator__elapsed")).not.toBeNull();
expect(container.querySelector(".chat-working-indicator__status")?.textContent).toContain(
"Snapping…",
"Working…",
);
expect(container.querySelector(".chat-group-footer")).toBeNull();
});
@@ -1095,7 +1093,7 @@ describe("grouped chat rendering", () => {
}
});
it("keeps one progress label stable per run while varying labels across runs", () => {
it("keeps the progress label plain across runs", () => {
const labelFor = (startedAt: number) => {
const container = document.createElement("div");
render(
@@ -1106,26 +1104,9 @@ describe("grouped chat rendering", () => {
?.textContent;
};
expect(labelFor(1_000)).toBe("Snapping…");
expect(labelFor(1_500)).toBe("Snapping…");
expect(labelFor(8_000)).toBe("Clawing…");
});
it("localizes branded progress labels", async () => {
i18n.registerTranslation("pt-BR", {
chat: { progressLabels: { snapping: "Estalando" } },
});
await i18n.setLocale("pt-BR");
const container = document.createElement("div");
render(
renderStreamGroup([{ kind: "reading-indicator", key: "reading", startedAt: 1_000 }]),
container,
);
expect(
container.querySelector(".chat-working-indicator__status span:last-child")?.textContent,
).toBe("Estalando…");
expect(labelFor(1_000)).toBe("Working…");
expect(labelFor(1_500)).toBe("Working…");
expect(labelFor(8_000)).toBe("Working…");
});
it("renders configured local user names", () => {
@@ -1,8 +1,4 @@
import { html } from "lit";
import {
DEFAULT_PROGRESS_DRAFT_LABELS,
selectProgressLabel,
} from "../../../../../src/shared/progress-labels.js";
import "../../../components/elapsed-time.ts";
import { icons } from "../../../components/icons.ts";
import { t } from "../../../i18n/index.ts";
@@ -16,14 +12,6 @@ const PUNCH_STANCES: Array<[stance: string, weight: number]> = [
["chat-reading-indicator--flurry", 12],
["chat-reading-indicator--haymaker", 6],
];
const CHAT_PROGRESS_LABELS = DEFAULT_PROGRESS_DRAFT_LABELS.slice(1);
function localizeProgressLabel(label: string): string {
const key = `chat.progressLabels.${label.toLowerCase()}`;
const translated = t(key);
return translated === key ? label : translated;
}
function punchStanceClass(key: string): string {
let hash = 0x811c9dc5;
for (let i = 0; i < key.length; i++) {
@@ -42,14 +30,6 @@ function punchStanceClass(key: string): string {
}
export function renderChatWorkingIndicator(part: Extract<ChatItem, { kind: "reading-indicator" }>) {
// Run start changes between turns but stays fixed across re-renders, so the
// phrase varies without flickering while the elapsed timer advances.
const selectedLabel =
selectProgressLabel({
labels: CHAT_PROGRESS_LABELS,
seed: String(Math.floor(part.startedAt / 1_000)),
}) ?? CHAT_PROGRESS_LABELS[0];
const progressLabel = localizeProgressLabel(selectedLabel ?? DEFAULT_PROGRESS_DRAFT_LABELS[0]);
// The animated claw stays decorative; the text status exposes progress without
// announcing every elapsed-time tick to screen readers.
return html`
@@ -66,7 +46,7 @@ export function renderChatWorkingIndicator(part: Extract<ChatItem, { kind: "read
.startMs=${part.startedAt}
></openclaw-elapsed-time>
<span aria-hidden="true">·</span>
<span>${progressLabel}</span>
<span>${t("common.working")}</span>
</span>
</div>
`;