From 3ea3184efe84b3c5a76570ab4d02a508530b7b0d Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Sun, 22 Feb 2026 06:27:06 -0600 Subject: [PATCH] refactor(ui): implement agent avatar resolution and logo fallback in agent rendering --- ui/src/ui/views/agents-utils.ts | 30 +++++++++++++++++++++++++++--- ui/src/ui/views/agents.ts | 27 ++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/ui/src/ui/views/agents-utils.ts b/ui/src/ui/views/agents-utils.ts index 29dbf90505c..8f24b9d0a01 100644 --- a/ui/src/ui/views/agents-utils.ts +++ b/ui/src/ui/views/agents-utils.ts @@ -138,6 +138,30 @@ export function normalizeAgentLabel(agent: { return agent.name?.trim() || agent.identity?.name?.trim() || agent.id; } +const AVATAR_URL_RE = /^(https?:\/\/|data:image\/|\/)/i; + +export function resolveAgentAvatarUrl( + agent: { identity?: { avatar?: string; avatarUrl?: string } }, + agentIdentity?: AgentIdentityResult | null, +): string | null { + const url = + agentIdentity?.avatar?.trim() ?? + agent.identity?.avatarUrl?.trim() ?? + agent.identity?.avatar?.trim(); + if (!url) { + return null; + } + if (AVATAR_URL_RE.test(url)) { + return url; + } + return null; +} + +export function agentLogoUrl(basePath: string): string { + const base = basePath?.trim() ? basePath.replace(/\/$/, "") : ""; + return base ? `${base}/favicon.svg` : "/favicon.svg"; +} + function isLikelyEmoji(value: string) { const trimmed = value.trim(); if (!trimmed) { @@ -229,7 +253,7 @@ export type AgentContext = { workspace: string; model: string; identityName: string; - identityEmoji: string; + identityAvatar: string; skillsLabel: string; isDefault: boolean; }; @@ -255,14 +279,14 @@ export function buildAgentContext( agent.name?.trim() || config.entry?.name || agent.id; - const identityEmoji = resolveAgentEmoji(agent, agentIdentity) || "-"; + const identityAvatar = resolveAgentAvatarUrl(agent, agentIdentity) ? "custom" : "—"; const skillFilter = Array.isArray(config.entry?.skills) ? config.entry?.skills : null; const skillCount = skillFilter?.length ?? null; return { workspace, model: modelLabel, identityName, - identityEmoji, + identityAvatar, skillsLabel: skillFilter ? `${skillCount} selected` : "all skills", isDefault: Boolean(defaultId && agent.id === defaultId), }; diff --git a/ui/src/ui/views/agents.ts b/ui/src/ui/views/agents.ts index 020798774c8..3d6fd3b80ef 100644 --- a/ui/src/ui/views/agents.ts +++ b/ui/src/ui/views/agents.ts @@ -18,9 +18,10 @@ import { renderAgentTools, renderAgentSkills } from "./agents-panels-tools-skill import { agentAvatarHue, agentBadgeText, + agentLogoUrl, buildAgentContext, normalizeAgentLabel, - resolveAgentEmoji, + resolveAgentAvatarUrl, } from "./agents-utils.ts"; export type AgentsPanel = "overview" | "files" | "tools" | "skills" | "channels" | "cron"; @@ -65,6 +66,7 @@ export type AgentSkillsState = { }; export type AgentsProps = { + basePath: string; loading: boolean; error: string | null; agentsList: AgentsListResult | null; @@ -174,8 +176,12 @@ export function renderAgents(props: AgentsProps) { ` : filteredAgents.map((agent) => { const badge = agentBadgeText(agent.id, defaultId); - const emoji = resolveAgentEmoji(agent, props.agentIdentityById[agent.id] ?? null); + const avatarUrl = resolveAgentAvatarUrl( + agent, + props.agentIdentityById[agent.id] ?? null, + ); const hue = agentAvatarHue(agent.id); + const logoUrl = agentLogoUrl(props.basePath); return html`