From bffce8ea4f07089c1ed5c38631fadcd6748b7369 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 03:46:30 +0000 Subject: [PATCH] fix(ui): harden avatar fallback regressions --- ui/src/ui/views/agents-utils.test.ts | 16 +++++++++ ui/src/ui/views/agents-utils.ts | 21 +++++++----- ui/src/ui/views/chat.test.ts | 49 ++++++++++++++++++++++++++++ vitest.config.ts | 1 + 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/ui/src/ui/views/agents-utils.test.ts b/ui/src/ui/views/agents-utils.test.ts index 8935520c2..d5a814b0d 100644 --- a/ui/src/ui/views/agents-utils.test.ts +++ b/ui/src/ui/views/agents-utils.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { agentLogoUrl, resolveConfiguredCronModelSuggestions, + resolveAgentAvatarUrl, resolveEffectiveModelFallbacks, sortLocaleStrings, } from "./agents-utils.ts"; @@ -110,3 +111,18 @@ describe("agentLogoUrl", () => { expect(agentLogoUrl("")).toBe("favicon.svg"); }); }); + +describe("resolveAgentAvatarUrl", () => { + it("prefers a runtime avatar URL over non-URL identity avatars", () => { + expect( + resolveAgentAvatarUrl({ identity: { avatar: "A", avatarUrl: "/avatar/main" } }, { + avatar: "A", + } as { avatar: string }), + ).toBe("/avatar/main"); + }); + + it("returns null for initials or emoji avatar values without a URL", () => { + expect(resolveAgentAvatarUrl({ identity: { avatar: "A" } })).toBeNull(); + expect(resolveAgentAvatarUrl({ identity: { avatar: "🦞" } })).toBeNull(); + }); +}); diff --git a/ui/src/ui/views/agents-utils.ts b/ui/src/ui/views/agents-utils.ts index 1eb28892b..e0c06c413 100644 --- a/ui/src/ui/views/agents-utils.ts +++ b/ui/src/ui/views/agents-utils.ts @@ -200,15 +200,18 @@ 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; + const candidates = [ + agentIdentity?.avatar?.trim(), + agent.identity?.avatarUrl?.trim(), + agent.identity?.avatar?.trim(), + ]; + for (const candidate of candidates) { + if (!candidate) { + continue; + } + if (AVATAR_URL_RE.test(candidate)) { + return candidate; + } } return null; } diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 341409a9b..36f7ca602 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -96,6 +96,55 @@ describe("chat view", () => { expect(logoImage?.getAttribute("src")).toBe("favicon.svg"); }); + it("keeps the welcome logo fallback under the mounted base path", () => { + const container = document.createElement("div"); + render( + renderChat( + createProps({ + assistantName: "Assistant", + assistantAvatar: "A", + assistantAvatarUrl: null, + basePath: "/openclaw/", + }), + ), + container, + ); + + const logoImage = container.querySelector( + ".agent-chat__welcome .agent-chat__avatar--logo img", + ); + expect(logoImage).not.toBeNull(); + expect(logoImage?.getAttribute("src")).toBe("/openclaw/favicon.svg"); + }); + + it("keeps grouped assistant avatar fallbacks under the mounted base path", () => { + const container = document.createElement("div"); + render( + renderChat( + createProps({ + assistantName: "Assistant", + assistantAvatar: "A", + assistantAvatarUrl: null, + basePath: "/openclaw/", + messages: [ + { + role: "assistant", + content: "hello", + timestamp: 1000, + }, + ], + }), + ), + container, + ); + + const groupedLogo = container.querySelector( + ".chat-group.assistant .chat-avatar--logo", + ); + expect(groupedLogo).not.toBeNull(); + expect(groupedLogo?.getAttribute("src")).toBe("/openclaw/favicon.svg"); + }); + it("renders compacting indicator as a badge", () => { const container = document.createElement("div"); render( diff --git a/vitest.config.ts b/vitest.config.ts index f164a8e2a..2c14f06a1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -82,6 +82,7 @@ export default defineConfig({ "src/**/*.test.ts", "extensions/**/*.test.ts", "test/**/*.test.ts", + "ui/src/ui/app-chat.test.ts", "ui/src/ui/views/agents-utils.test.ts", "ui/src/ui/views/chat.test.ts", "ui/src/ui/views/usage-render-details.test.ts",