Replace the inline hand-transcribed palette with the self-contained
src/lib/avatar-palette.ts: the 20-color palette is GENERATED at module load
from an OKLCH ring config (chroma clamped to sRGB, WCAG text color per color),
so it is fully tunable and validated (min pairwise ΔE-OK ≈ 0.066).
avatarStyle() slices one cyrb53 hash of the normalized name into independent
channels: base color (20) × color-wheel scheme (analogous ±20–45° / complement
180° / triadic ±120°) × split angle (24 dirs). avatarBackgroundCss() renders a
two-stop gradient with a soft boundary. Pure, cross-platform, deterministic —
same name → same avatar everywhere, nothing persisted.
The glyph now consumes avatarStyle/avatarBackgroundCss from the module;
agent-avatar-stack no longer defines its own hash/palette.
Tests: avatar-palette.test.ts pins minPairwiseDistance ≥ 0.06, PALETTE length,
normalization, and a golden name→style slice (Backend Developer →
#a55795/#90355e/150°) so a config change that repaints every avatar can't slip
through unnoticed. client tsc clean, 30 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the ad-hoc 14-color hsl palette with a perceptually-even, validated
scheme so agent glyphs are reliably distinguishable:
- cyrb53 deterministic, cross-platform 53-bit hash over a normalized name
(NFC + trim + lowercase + collapse whitespace) — no built-in/rand hash, so
the same name renders the same avatar on every device without persistence.
- 20-color OKLCH palette (12 light / 8 dark), chroma clamped to sRGB, min
pairwise ΔEOK ≈ 0.066: any two entries are identical or clearly distinct —
"almost the same" colors are impossible by construction.
- Disjoint hash-bit channels: base color (20) × gradient partner (2) ×
gradient angle (8) = 320 combinations, so a base-color collision (inevitable
past ~20 agents) is still disambiguated by the gradient — and by the emoji
drawn on top. Text color (black on light ring, white on dark) is
WCAG-checked.
Glyph now renders an explicit solid backgroundColor (fallback + testable) plus
a linear-gradient backgroundImage. avatarStyle() replaces agentGlyphBackground().
client tsc clean, 26 tests pass (avatarStyle determinism/normalization/structure
+ DOM base-color).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F1: the bug was the color never reaching the DOM (Mantine Avatar's --avatar-bg
overrode it); the pure agentGlyphBackground always returned distinct colors, so
the existing unit tests would pass even against the broken Avatar. Add a
data-testid on the glyph Box and two render tests: one asserts the emoji glyph's
applied inline background equals agentGlyphBackground(name); one asserts two
palette-distinct agents reach the DOM as different backgrounds. React applies
styles via the CSSOM (hsl→rgb), so the assertion normalizes both sides through
the same path and compares against the real function output (no frozen literal).
Fails against the pre-fix Avatar (no inline background / no glyph testid).
F2: the top-level AgentAvatarStack JSDoc and two test titles still described the
old z-order (agent glyph in front, human behind); the PR flipped it (human
launcher badge in front, zIndex 2 > glyph 1). Updated the JSDoc + both titles to
match.
vitest: 10 passed (+2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-agent glyph color never showed: the circle was a Mantine
`Avatar variant="filled"` whose background was overridden by Mantine's
`--avatar-bg`, so every agent fell back to the theme's violet. Also raw
`hue = hash % 360` put many names in the same "purple" arc.
- Render the emoji/sparkles circle as a plain Box with an explicit
background — the color is now guaranteed.
- Pick the color from a curated palette of categorically-distinct dark
hues (red/orange/green/teal/blue/violet/magenta/slate) by name hash, so
different agents read as different colors, not shades of one violet.
- Bring the launcher (human) badge ABOVE the agent glyph (zIndex) so it is
fully visible at the top-right instead of half-hidden behind the circle.
client tsc clean, tests pass (added a color-distinctness assertion).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
Two visual defects in the agent avatar stack (PR #304), missed by the
code-only review:
- The launcher (human) avatar was fully occluded behind the opaque agent
glyph — the container was exactly the glyph size, so the launcher sat
underneath it. Enlarge the container by an overhang and vertically center
the glyph so the launcher peeks out at the bottom-right and stays visible.
- On comments the human creator stayed the PRIMARY avatar and name while the
stack was crammed into the old badge slot, duplicating the identity and
failing the "agent is primary" requirement. AgentAvatarStack gains a
showName prop; with showName=false it now replaces the leading avatar for
agent comments, and the name slot renders agent.name (+ dimmed
· launcher.name). Non-agent comments are byte-identical to before;
history-item keeps the default (names shown).
Tests: add showName=false and external-MCP (no-launcher) coverage, assert
no identity duplication. client tsc clean, 9 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
For AI-agent-authored content (comments + page history), replace the text AI-AGENT
badge with an avatar stack: the agent in front, the human who launched it smaller and
behind. This fixes the inverted hierarchy (the action was the agent's; the human just
launched it). closes#300.
Backend: a single server-authoritative resolver resolveAgentProvenance normalizes to
{ agent, launcher } from server columns only (createdSource/lastUpdatedSource, aiChatId,
creator, chat role) — nothing from request input, so agent identity can't be spoofed.
Internal chat -> agent = chat role (name/emoji), launcher = human; external MCP
(aiChatId null) -> agent = the agent account, launcher = null; non-agent -> neither.
The role join (aiChatId -> ai_chats.role_id -> ai_agent_roles) deliberately does NOT
filter enabled/deleted_at, so a later-disabled role still labels historical content
(mirrors findById, not findLiveEnabled). Enrichment is applied on BOTH findPageComments
(list) AND findById (the create/resolve/update broadcast path), so the stack shows on
live comment events and doesn't vanish on resolve/edit.
Frontend: new AgentAvatarStack + AgentGlyph (avatarUrl -> role emoji on violet ->
IconSparkles on violet), integrated into comment-list-item and history-item where the
badge was; the deep-link-to-chat click moved onto the stack. ai-agent-badge removed.
Tests: AgentAvatarStack (role/no-role/MCP/click/non-clickable), the provenance resolver
+ recorder tests proving the role join never filters enabled/deleted, and findById
enrichment (guards the live-broadcast regression).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>