mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
feat(ui): sweep a text wave across the active tool row while it runs (#104266)
This commit is contained in:
committed by
GitHub
parent
54eda58447
commit
7da12cbbe5
@@ -175,4 +175,98 @@ describeControlUiE2e("Control UI autonomous tool-turn outcomes", () => {
|
||||
await captureToolActivityProof(page, "parallel-multifile-expanded");
|
||||
await context.close();
|
||||
});
|
||||
|
||||
it("sweeps a text wave over the active tool row and stops it on the result", async () => {
|
||||
const context = await browser.newContext({ viewport: { height: 800, width: 1200 } });
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
historyMessages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Ready for the running tool wave proof." }],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto(`${server.baseUrl}chat`);
|
||||
await page.getByText("Ready for the running tool wave proof.").waitFor();
|
||||
await page.locator(".agent-chat__input textarea").fill("run a long command");
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
const send = await gateway.waitForRequest("chat.send");
|
||||
const runId = (send.params as { idempotencyKey?: string }).idempotencyKey as string;
|
||||
|
||||
await gateway.emitGatewayEvent("agent", {
|
||||
runId,
|
||||
seq: 1,
|
||||
stream: "tool",
|
||||
ts: Date.now(),
|
||||
sessionKey: "main",
|
||||
data: {
|
||||
toolCallId: "call-wave",
|
||||
name: "exec",
|
||||
phase: "start",
|
||||
args: { command: "pnpm check:changed" },
|
||||
},
|
||||
});
|
||||
// Start-phase sync is throttled and repaints on the next event, so follow
|
||||
// with a delta (as real runs do) to surface the live card.
|
||||
await page.waitForTimeout(200);
|
||||
await gateway.emitGatewayEvent("chat", {
|
||||
deltaText: "Working on it.",
|
||||
message: {
|
||||
content: [{ text: "Working on it.", type: "text" }],
|
||||
role: "assistant",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
runId,
|
||||
sessionKey: "main",
|
||||
state: "delta",
|
||||
});
|
||||
await page.getByText("Working on it.").waitFor();
|
||||
|
||||
const runningRow = page.locator(".chat-tool-row--running");
|
||||
await runningRow.waitFor();
|
||||
// Visual-regression guard for the active-task text wave: the running
|
||||
// command text must carry the glyph-clipped gradient animation.
|
||||
const wave = await runningRow.locator(".chat-tool-row__cmd").evaluate((node) => {
|
||||
const style = getComputedStyle(node);
|
||||
return {
|
||||
animationName: style.animationName,
|
||||
backgroundClip: style.getPropertyValue("-webkit-background-clip") || style.backgroundClip,
|
||||
color: style.color,
|
||||
};
|
||||
});
|
||||
expect(wave.animationName).toBe("chatToolRowTextWave");
|
||||
expect(wave.backgroundClip).toBe("text");
|
||||
expect(wave.color).toBe("rgba(0, 0, 0, 0)");
|
||||
await captureToolActivityProof(page, "tool-row-running-text-wave");
|
||||
|
||||
await gateway.emitGatewayEvent("agent", {
|
||||
runId,
|
||||
seq: 2,
|
||||
stream: "tool",
|
||||
ts: Date.now(),
|
||||
sessionKey: "main",
|
||||
data: {
|
||||
toolCallId: "call-wave",
|
||||
name: "exec",
|
||||
phase: "result",
|
||||
result: { text: "done" },
|
||||
},
|
||||
});
|
||||
// The wave is a live-run marker only: the result event must end it and
|
||||
// restore plain text color even though the run has not finished yet.
|
||||
await expect.poll(() => page.locator(".chat-tool-row--running").count()).toBe(0);
|
||||
const settled = await page
|
||||
.locator(".chat-tool-row__cmd")
|
||||
.first()
|
||||
.evaluate((node) => {
|
||||
const style = getComputedStyle(node);
|
||||
return { animationName: style.animationName, color: style.color };
|
||||
});
|
||||
expect(settled.animationName).toBe("none");
|
||||
expect(settled.color).not.toBe("rgba(0, 0, 0, 0)");
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -195,6 +195,53 @@
|
||||
color: var(--accent-2, #14b8a6);
|
||||
}
|
||||
|
||||
/* Active-task text wave: a subtle left-to-right color sweep across the
|
||||
running row's primary text (same shimmer language as the skeleton/usage
|
||||
loaders, clipped to glyphs). The band fades text toward --bg so it stays
|
||||
correct in both themes. Scoped to no-preference so reduced-motion users
|
||||
keep plain static text and syntax colors. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.chat-tool-row--running :is(
|
||||
.chat-tool-msg-summary__label,
|
||||
.chat-tool-row__title,
|
||||
.chat-tool-row__verb,
|
||||
.chat-tool-row__target,
|
||||
.chat-tool-row__cmd:not(.chat-tool-row__cmd--secondary)
|
||||
) {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
var(--text) 0%,
|
||||
var(--text) 35%,
|
||||
color-mix(in srgb, var(--text) 42%, var(--bg) 58%) 50%,
|
||||
var(--text) 65%,
|
||||
var(--text) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: chatToolRowTextWave 2.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Syntax-highlight tokens go transparent while running so the parent's
|
||||
clipped gradient paints their glyphs; token colors return on completion. */
|
||||
.chat-tool-row--running .chat-tool-row__cmd:not(.chat-tool-row__cmd--secondary) > span {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gradient is 2x element width with identical end stops, so one full period
|
||||
per cycle wraps seamlessly and the band sweeps left to right. */
|
||||
@keyframes chatToolRowTextWave {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-tool-row__spinner {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
|
||||
Reference in New Issue
Block a user