refactor(ui): implement agent avatar resolution and logo fallback in agent rendering

This commit is contained in:
Val Alexander
2026-02-22 06:27:06 -06:00
parent 284961108a
commit 3ea3184efe
2 changed files with 49 additions and 8 deletions

View File

@@ -138,6 +138,30 @@ export function normalizeAgentLabel(agent: {
return agent.name?.trim() || agent.identity?.name?.trim() || agent.id; 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) { function isLikelyEmoji(value: string) {
const trimmed = value.trim(); const trimmed = value.trim();
if (!trimmed) { if (!trimmed) {
@@ -229,7 +253,7 @@ export type AgentContext = {
workspace: string; workspace: string;
model: string; model: string;
identityName: string; identityName: string;
identityEmoji: string; identityAvatar: string;
skillsLabel: string; skillsLabel: string;
isDefault: boolean; isDefault: boolean;
}; };
@@ -255,14 +279,14 @@ export function buildAgentContext(
agent.name?.trim() || agent.name?.trim() ||
config.entry?.name || config.entry?.name ||
agent.id; 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 skillFilter = Array.isArray(config.entry?.skills) ? config.entry?.skills : null;
const skillCount = skillFilter?.length ?? null; const skillCount = skillFilter?.length ?? null;
return { return {
workspace, workspace,
model: modelLabel, model: modelLabel,
identityName, identityName,
identityEmoji, identityAvatar,
skillsLabel: skillFilter ? `${skillCount} selected` : "all skills", skillsLabel: skillFilter ? `${skillCount} selected` : "all skills",
isDefault: Boolean(defaultId && agent.id === defaultId), isDefault: Boolean(defaultId && agent.id === defaultId),
}; };

View File

@@ -18,9 +18,10 @@ import { renderAgentTools, renderAgentSkills } from "./agents-panels-tools-skill
import { import {
agentAvatarHue, agentAvatarHue,
agentBadgeText, agentBadgeText,
agentLogoUrl,
buildAgentContext, buildAgentContext,
normalizeAgentLabel, normalizeAgentLabel,
resolveAgentEmoji, resolveAgentAvatarUrl,
} from "./agents-utils.ts"; } from "./agents-utils.ts";
export type AgentsPanel = "overview" | "files" | "tools" | "skills" | "channels" | "cron"; export type AgentsPanel = "overview" | "files" | "tools" | "skills" | "channels" | "cron";
@@ -65,6 +66,7 @@ export type AgentSkillsState = {
}; };
export type AgentsProps = { export type AgentsProps = {
basePath: string;
loading: boolean; loading: boolean;
error: string | null; error: string | null;
agentsList: AgentsListResult | null; agentsList: AgentsListResult | null;
@@ -174,8 +176,12 @@ export function renderAgents(props: AgentsProps) {
` `
: filteredAgents.map((agent) => { : filteredAgents.map((agent) => {
const badge = agentBadgeText(agent.id, defaultId); 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 hue = agentAvatarHue(agent.id);
const logoUrl = agentLogoUrl(props.basePath);
return html` return html`
<button <button
type="button" type="button"
@@ -183,7 +189,11 @@ export function renderAgents(props: AgentsProps) {
@click=${() => props.onSelectAgent(agent.id)} @click=${() => props.onSelectAgent(agent.id)}
> >
<div class="agent-avatar" style="--agent-hue: ${hue}"> <div class="agent-avatar" style="--agent-hue: ${hue}">
${emoji || normalizeAgentLabel(agent).slice(0, 1)} ${
avatarUrl
? html`<img src=${avatarUrl} alt="" class="agent-avatar__img" />`
: html`<img src=${logoUrl} alt="" class="agent-avatar__img agent-avatar__logo" />`
}
</div> </div>
<div class="agent-info"> <div class="agent-info">
<div class="agent-title">${normalizeAgentLabel(agent)}</div> <div class="agent-title">${normalizeAgentLabel(agent)}</div>
@@ -211,6 +221,7 @@ export function renderAgents(props: AgentsProps) {
defaultId, defaultId,
props.agentIdentityById[selectedAgent.id] ?? null, props.agentIdentityById[selectedAgent.id] ?? null,
props.onSetDefault, props.onSetDefault,
props.basePath,
)} )}
${renderAgentTabs(props.activePanel, (panel) => props.onSelectPanel(panel), tabCounts)} ${renderAgentTabs(props.activePanel, (panel) => props.onSelectPanel(panel), tabCounts)}
${ ${
@@ -341,11 +352,12 @@ function renderAgentHeader(
defaultId: string | null, defaultId: string | null,
agentIdentity: AgentIdentityResult | null, agentIdentity: AgentIdentityResult | null,
onSetDefault: (agentId: string) => void, onSetDefault: (agentId: string) => void,
basePath: string,
) { ) {
const badge = agentBadgeText(agent.id, defaultId); const badge = agentBadgeText(agent.id, defaultId);
const displayName = normalizeAgentLabel(agent); const displayName = normalizeAgentLabel(agent);
const subtitle = agent.identity?.theme?.trim() || "Agent workspace and routing."; const subtitle = agent.identity?.theme?.trim() || "Agent workspace and routing.";
const emoji = resolveAgentEmoji(agent, agentIdentity); const avatarUrl = resolveAgentAvatarUrl(agent, agentIdentity);
const hue = agentAvatarHue(agent.id); const hue = agentAvatarHue(agent.id);
const isDefault = Boolean(defaultId && agent.id === defaultId); const isDefault = Boolean(defaultId && agent.id === defaultId);
@@ -354,11 +366,16 @@ function renderAgentHeader(
actionsMenuOpen = false; actionsMenuOpen = false;
}; };
const logoUrl = agentLogoUrl(basePath);
return html` return html`
<section class="card agent-header"> <section class="card agent-header">
<div class="agent-header-main"> <div class="agent-header-main">
<div class="agent-avatar agent-avatar--lg" style="--agent-hue: ${hue}"> <div class="agent-avatar agent-avatar--lg" style="--agent-hue: ${hue}">
${emoji || displayName.slice(0, 1)} ${
avatarUrl
? html`<img src=${avatarUrl} alt="" class="agent-avatar__img" />`
: html`<img src=${logoUrl} alt="" class="agent-avatar__img agent-avatar__logo" />`
}
</div> </div>
<div> <div>
<div class="card-title">${displayName}</div> <div class="card-title">${displayName}</div>