diff --git a/apps/client/src/components/ui/agent-avatar-stack.test.tsx b/apps/client/src/components/ui/agent-avatar-stack.test.tsx index 9a6460ab..39d446b1 100644 --- a/apps/client/src/components/ui/agent-avatar-stack.test.tsx +++ b/apps/client/src/components/ui/agent-avatar-stack.test.tsx @@ -43,6 +43,24 @@ describe("AgentAvatarStack", () => { expect(screen.getByText("Alice")).toBeDefined(); }); + it("showName=false: renders only the avatars, no inline name label", () => { + renderStack({ + agent: { name: "Researcher", emoji: "🔬", avatarUrl: null }, + launcher: { name: "Alice", avatarUrl: null }, + aiChatId: "chat-1", + showName: false, + }); + + // The agent glyph is still rendered... + expect(screen.getByText("🔬")).toBeDefined(); + // ...but neither the agent NOR the launcher inline name label is rendered + // (they live only in the hover tooltip, which is not mounted in the initial + // DOM) — guards against suppressing only the agent name and leaking the + // launcher name. + expect(screen.queryByText("Researcher")).toBeNull(); + expect(screen.queryByText("Alice")).toBeNull(); + }); + it("internal chat WITHOUT role: sparkles fallback + 'AI agent' + launcher", () => { const { container } = renderStack({ agent: { name: "AI agent", avatarUrl: null }, diff --git a/apps/client/src/components/ui/agent-avatar-stack.tsx b/apps/client/src/components/ui/agent-avatar-stack.tsx index 430c10c4..59ce252c 100644 --- a/apps/client/src/components/ui/agent-avatar-stack.tsx +++ b/apps/client/src/components/ui/agent-avatar-stack.tsx @@ -27,6 +27,9 @@ export interface LauncherInfo { const AGENT_COLOR = "violet"; const GLYPH_SIZE = 38; const LAUNCHER_SIZE = 22; +// How far the launcher avatar sticks out past the agent's bottom-right corner, so +// the "human behind" reads as behind (lower z-index) yet stays clearly visible. +const LAUNCHER_OVERHANG = 8; /** * The front avatar. Image-source priority (#300): @@ -72,6 +75,9 @@ export interface AgentAvatarStackProps { // (e.g. the page-history row closes the history modal). Keeps this ui/ primitive // free of cross-feature coupling (inherited from the old AiAgentBadge, #143). onActivate?: () => void; + // Whether to render the inline name label next to the avatars (default true). + // Set false when the caller renders the name itself (e.g. the comment row). + showName?: boolean; } /** @@ -87,6 +93,7 @@ export function AgentAvatarStack({ launcher, aiChatId, onActivate, + showName = true, }: AgentAvatarStackProps) { const { t } = useTranslation(); const setAiChatWindowOpen = useSetAtom(aiChatWindowOpenAtom); @@ -117,13 +124,21 @@ export function AgentAvatarStack({ }) : t("AI agent {{name}}", { name: agent.name }); + // The container is only enlarged when there is a launcher to overhang; with no + // human behind it stays tight at the agent glyph size. + const stackSize = launcher ? GLYPH_SIZE + LAUNCHER_OVERHANG : GLYPH_SIZE; + const stack = ( )} - + {/* Pin the agent glyph to the top-left at its own size; the launcher then + overhangs it by LAUNCHER_OVERHANG at the bottom-right and stays visible. */} + @@ -161,21 +185,23 @@ export function AgentAvatarStack({ {stack} - - - {agent.name} - - {launcher && ( - <> - - · - - - {launcher.name} - - - )} - + {showName && ( + + + {agent.name} + + {launcher && ( + <> + + · + + + {launcher.name} + + + )} + + )} ); } diff --git a/apps/client/src/features/comment/components/comment-list-item.test.tsx b/apps/client/src/features/comment/components/comment-list-item.test.tsx index d1034e4a..1826e4ae 100644 --- a/apps/client/src/features/comment/components/comment-list-item.test.tsx +++ b/apps/client/src/features/comment/components/comment-list-item.test.tsx @@ -41,19 +41,39 @@ function renderItem(comment: IComment) { } describe("CommentListItem — agent avatar stack", () => { - it('renders the agent avatar stack when createdSource === "agent"', () => { - // External-MCP shape: agent is the account itself, no launcher behind. + it('flips the hierarchy for an agent comment: agent primary, launcher shown once', () => { + // Internal-chat shape with DISTINCT names so absence-of-duplication is + // assertable: creator is the human "Alice", the acting agent is "Researcher". renderItem( baseComment({ + creator: { id: "user-1", name: "Alice", avatarUrl: null } as any, + createdSource: "agent", + aiChatId: "chat-1", + agent: { name: "Researcher", emoji: "🔬", avatarUrl: null }, + launcher: { name: "Alice", avatarUrl: null }, + }), + ); + // The AGENT is the primary label (the flipped hierarchy). + expect(screen.getByText("Researcher")).toBeDefined(); + // The human launcher name shows exactly once — it is no longer duplicated as + // a separate creator name (that duplication is the bug this fixes). + expect(screen.getAllByText("Alice").length).toBe(1); + }); + + it('external MCP agent comment (no launcher): shows the agent name, no separator', () => { + // aiChatId null => external MCP: the agent IS the account, no human behind. + renderItem( + baseComment({ + creator: { id: "bot-1", name: "MCP Bot", avatarUrl: null } as any, createdSource: "agent", aiChatId: null, - agent: { name: "Service Bot", avatarUrl: null }, + agent: { name: "MCP Bot", avatarUrl: null }, launcher: null, }), ); - // The stack renders the agent name label (the creator name is also shown in - // the row header, so it appears more than once). - expect(screen.getAllByText("Service Bot").length).toBeGreaterThan(0); + expect(screen.getByText("MCP Bot")).toBeDefined(); + // No launcher => no dimmed "·" separator in the header. + expect(screen.queryByText("·")).toBeNull(); }); it('does NOT render the stack for a normal user comment (createdSource "user")', () => { diff --git a/apps/client/src/features/comment/components/comment-list-item.tsx b/apps/client/src/features/comment/components/comment-list-item.tsx index 7167ebdb..072281bb 100644 --- a/apps/client/src/features/comment/components/comment-list-item.tsx +++ b/apps/client/src/features/comment/components/comment-list-item.tsx @@ -119,25 +119,44 @@ function CommentListItem({ return ( - + {comment.createdSource === "agent" && comment.agent ? ( + + ) : ( + + )}
- - {comment.creator.name} - - - {comment.createdSource === "agent" && comment.agent && ( - + {comment.createdSource === "agent" && comment.agent ? ( + <> + + {comment.agent.name} + + {comment.launcher && ( + <> + + · + + + {comment.launcher.name} + + + )} + + ) : ( + + {comment.creator.name} + )}