refactor(ui): unify working indicator status rendering (#133713)

This commit is contained in:
Peter Steinberger 2026-08-30 19:12:19 -07:00 committed by GitHub
parent 9f6d8c5a60
commit e8d767bd2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 32 additions and 39 deletions

View file

@ -1788,6 +1788,7 @@ describe("grouped chat rendering", () => {
);
expect(container.querySelector(".chat-working-indicator__elapsed")).not.toBeNull();
expect(container.querySelector(".chat-working-indicator__status > .sr-only")).toBeNull();
expect(container.querySelector("openclaw-working-phrase")).toBeNull();
});
it("formats terminal recap durations with full localized units", () => {
@ -1815,19 +1816,23 @@ describe("grouped chat rendering", () => {
).toBe("Done in 30 seconds · 2,400 output tokens");
});
it("shows live output usage beside elapsed time", () => {
it.each([
[0, "0 output tokens"],
[1, "1 output token"],
[5_500, "5,500 output tokens"],
])("shows %i output tokens beside elapsed time", (outputTokens, label) => {
const container = document.createElement("div");
render(
renderStreamGroup([{ kind: "reading-indicator", key: "reading", startedAt: 1_000 }], {
runOutputTokens: 5_500,
runOutputTokens: outputTokens,
}),
container,
);
expect(container.querySelector(".chat-working-indicator__elapsed")).not.toBeNull();
expect(container.querySelector(".chat-working-indicator__tokens")?.textContent?.trim()).toBe(
"5,500 output tokens",
label,
);
// Known usage replaces the pre-usage working phrase.
expect(container.querySelector("openclaw-working-phrase")).toBeNull();

View file

@ -46,16 +46,6 @@ function outputTokensLabel(outputTokens: number): string {
: t("chat.turnRecap.tokens", { count: outputTokens.toLocaleString(i18n.getLocale()) });
}
function renderLiveOutputTokens(outputTokens: number | null | undefined) {
if (outputTokens === null || outputTokens === undefined) {
return nothing;
}
return html`
<span aria-hidden="true">·</span>
<span class="chat-working-indicator__tokens">${outputTokensLabel(outputTokens)}</span>
`;
}
export function renderChatWorkingIndicator(
part: Extract<ChatItem, { kind: "reading-indicator" }>,
options: {
@ -67,9 +57,13 @@ export function renderChatWorkingIndicator(
) {
const waitingApproval = options.waitingApproval === true;
const continuation = options.presentation === "continuation";
const statusLabel = waitingApproval
? t("chat.waitingForApproval")
: options.startupLabel || t("common.working");
const working = !waitingApproval && !options.startupLabel;
// Providers report exact usage at response boundaries, not per text delta.
// Keep the latest count visible while the run continues through tools.
const hasTokens = options.outputTokens !== null && options.outputTokens !== undefined;
const outputTokens = options.outputTokens;
// The animated claw stays decorative; the text status exposes progress without
// announcing every elapsed-time tick to screen readers.
return html`
@ -91,35 +85,29 @@ export function renderChatWorkingIndicator(
</div>
`}
<span class="chat-working-indicator__status">
<span class=${working && !continuation ? "sr-only" : ""}>${statusLabel}</span>
${waitingApproval
? html`<span>${t("chat.waitingForApproval")}</span>${renderLiveOutputTokens(
options.outputTokens,
)}`
: options.startupLabel
? nothing
: html`
<openclaw-elapsed-time
class="chat-working-indicator__elapsed"
.startMs=${part.startedAt}
></openclaw-elapsed-time>
`}
${outputTokens !== null && outputTokens !== undefined
? html`
<span aria-hidden="true">·</span>
<span class="chat-working-indicator__tokens">${outputTokensLabel(outputTokens)}</span>
`
: working
? html`
<span>${options.startupLabel}</span>
<openclaw-elapsed-time
class="chat-working-indicator__elapsed"
<openclaw-working-phrase
aria-hidden="true"
.startMs=${part.startedAt}
></openclaw-elapsed-time>
${renderLiveOutputTokens(options.outputTokens)}
.seed=${part.key}
></openclaw-working-phrase>
`
: html`
<span class=${continuation ? "" : "sr-only"}>${t("common.working")}</span>
<openclaw-elapsed-time
class="chat-working-indicator__elapsed"
.startMs=${part.startedAt}
></openclaw-elapsed-time>
${hasTokens
? renderLiveOutputTokens(options.outputTokens)
: html`
<openclaw-working-phrase
aria-hidden="true"
.startMs=${part.startedAt}
.seed=${part.key}
></openclaw-working-phrase>
`}
`}
: nothing}
</span>
</div>
`;