From ea89b930938c3393ac8830233d85cf5f332a1e0c Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Wed, 22 Jul 2026 17:07:09 -0700 Subject: [PATCH] Stack team card avatars --- .../features/agents/ui/TeamIdentityCard.tsx | 36 ++++++++++--- desktop/tests/e2e/agents.spec.ts | 50 +++++++++++++++++++ 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/desktop/src/features/agents/ui/TeamIdentityCard.tsx b/desktop/src/features/agents/ui/TeamIdentityCard.tsx index 824b20660..b7d71c6a4 100644 --- a/desktop/src/features/agents/ui/TeamIdentityCard.tsx +++ b/desktop/src/features/agents/ui/TeamIdentityCard.tsx @@ -115,6 +115,7 @@ function TeamAvatarRow({ }) { const visiblePersonas = personas.slice(0, MAX_VISIBLE_MEMBER_AVATARS); const overflowCount = Math.max(0, memberCount - visiblePersonas.length); + const stackItemCount = visiblePersonas.length + (overflowCount > 0 ? 1 : 0); if (visiblePersonas.length === 0 && overflowCount === 0) { return ( @@ -130,16 +131,26 @@ function TeamAvatarRow({
{visiblePersonas.map((persona, index) => ( - + ))} {overflowCount > 0 ? ( - - +{overflowCount} - +
0 ? "-ml-5" : ""} + style={{ zIndex: stackItemCount }} + > + + +{overflowCount} + +
) : null}
@@ -148,15 +159,28 @@ function TeamAvatarRow({ function TeamAvatarItem({ index, + isFollowedByAnother, persona, }: { index: number; + isFollowedByAnother: boolean; persona: AgentPersona; }) { const avatarUrl = persona.avatarUrl?.trim() ?? null; return ( -
+
0 ? "-ml-5" : ""}`} + data-team-member-avatar="avatar" + style={{ + zIndex: index + 1, + ...(isFollowedByAnother && { + mask: "radial-gradient(circle 32px at calc(100% + 8px) 50%, transparent 99%, #fff 100%)", + WebkitMask: + "radial-gradient(circle 32px at calc(100% + 8px) 50%, transparent 99%, #fff 100%)", + }), + }} + > {avatarUrl ? ( { ).toBeVisible(); }); +test("team cards use the thread-style overlapping avatar stack", async ({ + page, +}) => { + const personaIds = ["custom:design", "custom:build", "custom:ship"]; + await installMockBridge(page, { + personas: [ + { + id: personaIds[0], + displayName: "Design", + systemPrompt: "You design interfaces.", + }, + { + id: personaIds[1], + displayName: "Build", + systemPrompt: "You build interfaces.", + }, + { + id: personaIds[2], + displayName: "Ship", + systemPrompt: "You ship interfaces.", + }, + ], + teams: [ + { + name: "Product crew", + personaIds, + }, + ], + }); + await gotoApp(page); + await page.getByTestId("open-agents-view").click(); + + const stack = page.getByLabel("Product crew member avatars"); + const avatars = stack.locator('[data-team-member-avatar="avatar"]'); + await expect(avatars).toHaveCount(3); + await expect(avatars.nth(1)).toHaveClass(/-ml-5/); + await expect(avatars.nth(2)).toHaveClass(/-ml-5/); + + const boxes = await avatars.evaluateAll((elements) => + elements.map((element) => { + const box = element.getBoundingClientRect(); + return { left: box.left, right: box.right }; + }), + ); + expect(boxes[1]?.left).toBeLessThan(boxes[0]?.right ?? 0); + expect(boxes[2]?.left).toBeLessThan(boxes[1]?.right ?? 0); + await expect(avatars.first()).not.toHaveCSS("mask-image", "none"); + await expect(avatars.last()).toHaveCSS("mask-image", "none"); +}); + test("agent defaults stays in the header without an actions menu", async ({ page, }) => {