Make the accent shimmer contrasty and robust to mid-turn attach

Contrast: the accent shimmer band swept raw primary, which sits too
close to the text's lightness in both themes (58%-L purple vs
near-black text in light; 80%-L purple vs near-white text in dark) and
barely read. The band now mixes primary toward the background — lighter
in light mode, darker in dark mode, both directions pulling away from
the text — and narrows so the highlight covers more visible text per
pass.

Robustness: observer frames are ephemeral, so an app subscribing
mid-turn missed turn_started and the turns store stayed empty (dark
sidebar shimmer, no working badges) until a liveness ping happened to
land. Any frame carrying a turn context — tool calls included, which
stream constantly during work — now establishes the turn, still gated
by the terminal tombstone so completed turns stay dead.

Adds an e2e spec covering the sidebar title shimmer end to end via
seeded active turns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
klopez4212
2026-07-07 07:46:18 +01:00
co-authored by Claude Fable 5
parent 463c74d5a2
commit ba8ac20fec
4 changed files with 132 additions and 7 deletions
@@ -1107,6 +1107,52 @@ describe("activeAgentTurnsStore", () => {
);
});
it("establishes a turn from a tool frame when turn_started was never seen", () => {
// Observer frames are ephemeral: an app subscribing mid-turn missed
// turn_started. The first in-turn frame of ANY kind must establish the
// turn so working indicators light up immediately.
syncAgentTurnsFromEvents(AGENT, [
makeEvent({
seq: 1,
kind: "tool_call",
turnId: "t1",
channelId: "c1",
timestamp: at(0),
}),
]);
assert.ok(
channelIdsOf(getActiveTurnsForAgent(AGENT)).has("c1"),
"a mid-turn tool frame must establish the turn without turn_started",
);
});
it("does NOT establish a turn from a tool frame older than its completion", () => {
// The tombstone gate applies to the establish path too: frames from a
// turn that already terminally ended must not revive it.
syncAgentTurnsFromEvents(AGENT, [
makeEvent({ seq: 1, turnId: "t1", channelId: "c1", timestamp: at(0) }),
makeEvent({
seq: 2,
kind: "turn_completed",
turnId: "t1",
channelId: "c1",
timestamp: at(10_000),
}),
makeEvent({
seq: 3,
kind: "tool_call",
turnId: "t1",
channelId: "c1",
timestamp: at(8_000),
}),
]);
assert.equal(
getActiveTurnsForAgent(AGENT).length,
0,
"a stale tool frame must not revive a completed turn",
);
});
it("does NOT resurrect from a liveness frame with no channelId", () => {
// A pruned turn cannot be rebuilt without a channelId to anchor the badge.
syncAgentTurnsFromEvents(AGENT, [
@@ -354,12 +354,23 @@ function processEvent(agentPubkey: string, event: ObserverEvent) {
return;
case "acp_read":
case "acp_write":
case "turn_liveness":
// Any other frame carrying a turn context (tool calls, assistant text,
// …) is equally hard evidence the turn is alive. Observer frames are
// ephemeral — an app that subscribes mid-turn missed `turn_started`
// entirely — so every in-turn frame must be able to establish the turn,
// not just refresh it. Without this, a chat can stream visible activity
// while the working indicators (sidebar shimmer, badges) stay dark
// until the next liveness ping happens to land.
//
// turn_liveness keeps a quiet-but-alive turn from being pruned; same
// refresh-only path as stream activity — no surfaced summary change on its
// own, so it only notifies when the offset above actually moved. If the
// turn was pruned out from under a still-running host (a transient drop
// raced the pause, or the lone-crash residual self-healed), resurrect it.
case "turn_liveness": {
// refresh-only path as stream activity — no surfaced summary change on
// its own, so it only notifies when the offset above actually moved. If
// the turn was pruned out from under a still-running host (a transient
// drop raced the pause, or the lone-crash residual self-healed), or was
// never seen to start, resurrect it. The terminal tombstone still keeps
// frames from a completed turn (or stale replays) from reviving it.
default: {
const refreshed = recordActivity(agentPubkey, event.turnId ?? null);
if (!refreshed && resurrectTurn(agentPubkey, event)) {
notifyListeners();
@@ -211,9 +211,22 @@
/* Accent variant: keeps the text at its normal color and sweeps a
primary-colored band across it, for places (sidebar titles) where the
default dimmed-base treatment reads as a de-emphasized row. */
default dimmed-base treatment reads as a de-emphasized row.
The band must contrast with the TEXT it sweeps (full foreground — this
variant never dims), so the primary hue is pulled toward the background:
lighter in light mode (against near-black text), darker in dark mode
(against near-white text). Raw hsl(var(--primary)) sat too close to the
text's lightness in both themes and the sweep barely read. The narrower
band (300% vs 400%) also makes the highlight cover more of the visible
text per pass. */
.buzz-shimmer-accent {
--buzz-shimmer-highlight: hsl(var(--primary));
--buzz-shimmer-highlight: color-mix(
in oklab,
hsl(var(--primary)) 55%,
hsl(var(--background))
);
--buzz-shimmer-band: 300%;
color: inherit;
}
@@ -174,3 +174,58 @@ test("first message in a new chat is sent and rendered", async ({ page }) => {
await waitForAnimations(page);
await page.screenshot({ path: "test-results/agent-pr-card.png" });
});
test("sidebar chat title shimmers while the agent has an active turn", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/#/chats");
const composer = page.locator("[contenteditable='true'], textarea").first();
await expect(composer).toBeVisible();
await composer.click();
await composer.fill("Spin up a new worktree to look at the panel");
await composer.press("Enter");
await expect(page).toHaveURL(/\/chats\/.+/);
const chatId = decodeURIComponent(
new URL(page.url().replace("/#/", "/")).pathname.split("/").at(-1) ?? "",
);
expect(chatId.length).toBeGreaterThan(0);
// Quiet agent: no shimmer on the sidebar title.
const shimmerTitle = page.locator(
"[data-shimmer-text*='Spin up a new worktree']",
);
await expect(shimmerTitle).toHaveCount(0);
// Seed an active turn for this chat — the row title must pick up the
// accent shimmer (class + animated overlay), same store the working row
// reads.
await page.evaluate(
({ channelId }) => {
(
window as Window & {
__BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: {
agentPubkey: string;
channelId: string;
turnId: string;
}) => void;
}
).__BUZZ_E2E_SEED_ACTIVE_TURNS__?.({
agentPubkey: "ab".repeat(32),
channelId,
turnId: "turn-shimmer-1",
});
},
{ channelId: chatId },
);
await expect(shimmerTitle).toHaveCount(1);
await expect(shimmerTitle).toHaveClass(/buzz-shimmer/);
await expect(shimmerTitle).toHaveClass(/buzz-shimmer-accent/);
expect(
await shimmerTitle.evaluate(
(element) => window.getComputedStyle(element, "::before").animationName,
),
).toBe("buzz-shimmer");
});