From 6f7d439811355e19280e3cc134da2226d67c23d6 Mon Sep 17 00:00:00 2001 From: claude_code Date: Fri, 3 Jul 2026 16:28:06 +0300 Subject: [PATCH 1/2] feat(#300 ui): move launcher to top-right, per-agent dark glyph color - Launcher (human) avatar moves from the bottom-right to the TOP-RIGHT corner of the agent glyph. - The emoji/sparkles glyph circle is no longer a fixed violet: its background is derived from a hash of the agent name (hue) and pinned to a fixed dark shade (hsl(h, 45%, 24%)) so distinct agents get distinct colors while the emoji / white sparkles icon stays readable. Agents with an uploaded avatar image are unaffected. Add a unit test for agentGlyphBackground (deterministic, name-varying, dark). client tsc clean, 11 tests pass. Co-Authored-By: Claude Opus 4.8 --- .../components/ui/agent-avatar-stack.test.tsx | 19 +++++++- .../src/components/ui/agent-avatar-stack.tsx | 45 +++++++++++++++---- 2 files changed, 55 insertions(+), 9 deletions(-) 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 39d446b1..545794e7 100644 --- a/apps/client/src/components/ui/agent-avatar-stack.test.tsx +++ b/apps/client/src/components/ui/agent-avatar-stack.test.tsx @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { MantineProvider } from "@mantine/core"; import { Provider, createStore } from "jotai"; -import { AgentAvatarStack } from "./agent-avatar-stack"; +import { AgentAvatarStack, agentGlyphBackground } from "./agent-avatar-stack"; import { activeAiChatIdAtom, aiChatWindowOpenAtom, @@ -26,6 +26,23 @@ function renderStack(props: Props) { return { store, ...utils }; } +describe("agentGlyphBackground", () => { + it("is deterministic for a given agent name", () => { + expect(agentGlyphBackground("Researcher")).toBe( + agentGlyphBackground("Researcher"), + ); + }); + + it("differs by name and stays a fixed dark shade (readable emoji)", () => { + expect(agentGlyphBackground("Researcher")).not.toBe( + agentGlyphBackground("Нарратор"), + ); + // Only the hue varies; saturation/lightness are pinned low so the glyph is + // always a dark circle. + expect(agentGlyphBackground("Нарратор")).toMatch(/^hsl\(\d+, 45%, 24%\)$/); + }); +}); + describe("AgentAvatarStack", () => { it("internal chat WITH role: emoji glyph in front + human launcher behind", () => { const { container } = renderStack({ diff --git a/apps/client/src/components/ui/agent-avatar-stack.tsx b/apps/client/src/components/ui/agent-avatar-stack.tsx index 59ce252c..fdf672c2 100644 --- a/apps/client/src/components/ui/agent-avatar-stack.tsx +++ b/apps/client/src/components/ui/agent-avatar-stack.tsx @@ -23,14 +23,34 @@ export interface LauncherInfo { avatarUrl?: string | null; } -// Same violet token as the former AiAgentBadge (which used color="violet"). -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 +// How far the launcher avatar sticks out past the agent's top-right corner, so // the "human behind" reads as behind (lower z-index) yet stays clearly visible. const LAUNCHER_OVERHANG = 8; +// Small deterministic string hash (same algorithm as custom-avatar's initials +// hash) used to pick a stable per-agent glyph color. +function hashName(input: string): number { + let hash = 0; + for (let i = 0; i < input.length; i += 1) { + hash = (hash << 5) - hash + input.charCodeAt(i); + hash |= 0; + } + return Math.abs(hash); +} + +/** + * Deterministic DARK background for an emoji/sparkles agent glyph. The hue is + * derived from the agent-name hash so distinct agents get distinct circles; + * saturation and lightness are pinned low ("shifted into darkness") so a bright + * emoji or the white sparkles icon stays legible on top (#300). + */ +export function agentGlyphBackground(name: string): string { + const hue = hashName(name) % 360; + return `hsl(${hue}, 45%, 24%)`; +} + /** * The front avatar. Image-source priority (#300): * 1. agent.avatarUrl -> a real avatar image (external MCP agent account). @@ -48,9 +68,18 @@ function AgentGlyph({ agent }: { agent: AgentInfo }) { ); } + // Emoji/sparkles glyphs sit on a per-agent dark circle (hashed from the agent + // name) so different agents are visually distinct, while the dark background + // keeps the emoji / white sparkles icon readable. + const bg = agentGlyphBackground(agent.name); + const glyphStyles = { + root: { background: bg }, + placeholder: { background: bg, color: "var(--mantine-color-white)" }, + }; + if (agent.emoji) { return ( - + {agent.emoji} @@ -59,7 +88,7 @@ function AgentGlyph({ agent }: { agent: AgentInfo }) { } return ( - + ); @@ -156,7 +185,7 @@ export function AgentAvatarStack({ : {})} > {launcher && ( - + )} - {/* 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. */} + {/* The agent glyph keeps its own size (flex-centered in the container); the + launcher overhangs it by LAUNCHER_OVERHANG at the top-right and stays visible. */} Date: Fri, 3 Jul 2026 18:01:18 +0300 Subject: [PATCH 2/2] =?UTF-8?q?docs(ai-chat):=20correct=20AgentGlyph=20doc?= =?UTF-8?q?string=20=E2=80=94=20per-agent=20dark=20circle,=20not=20violet?= =?UTF-8?q?=20(#307=20F1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The launcher-polish commit replaced the fixed violet AGENT_COLOR background with a per-agent dark hashed circle (agentGlyphBackground). Points 2 and 3 of the AgentGlyph image-source docstring still said 'violet circle' — update both to 'per-agent dark circle' so the doc matches the code. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/client/src/components/ui/agent-avatar-stack.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/components/ui/agent-avatar-stack.tsx b/apps/client/src/components/ui/agent-avatar-stack.tsx index fdf672c2..85eb2fe2 100644 --- a/apps/client/src/components/ui/agent-avatar-stack.tsx +++ b/apps/client/src/components/ui/agent-avatar-stack.tsx @@ -54,8 +54,8 @@ export function agentGlyphBackground(name: string): string { /** * The front avatar. Image-source priority (#300): * 1. agent.avatarUrl -> a real avatar image (external MCP agent account). - * 2. agent.emoji -> the role emoji on a violet circle. - * 3. otherwise -> the IconSparkles glyph on a violet circle (fallback). + * 2. agent.emoji -> the role emoji on a per-agent dark circle. + * 3. otherwise -> the IconSparkles glyph on a per-agent dark circle (fallback). */ function AgentGlyph({ agent }: { agent: AgentInfo }) { if (agent.avatarUrl) { -- 2.52.0