fix(ui): center settings help glyph, shrink custodian thinking mascot, add user avatar error fallback (#118337)

This commit is contained in:
Peter Steinberger
2026-08-02 18:00:13 -07:00
committed by GitHub
parent 18c9f27e2d
commit 899be356ac
4 changed files with 61 additions and 25 deletions
+16
View File
@@ -86,6 +86,22 @@ describe("renderChatAvatar", () => {
expect(textAvatar?.tagName).toBe("DIV");
expect(textAvatar?.textContent?.trim()).toBe("AB");
});
it("swaps a failing local user image to initials instead of a broken image", () => {
const container = document.createElement("div");
render(
renderChatAvatar("user", undefined, { name: "Buns", avatar: "/avatar/user" }),
container,
);
const slot = container.querySelector<HTMLElement>(".chat-avatar-slot");
const image = slot?.querySelector("img");
expect(image?.getAttribute("src")).toBe("/avatar/user");
expect(slot?.classList.contains("is-fallback")).toBe(false);
image?.dispatchEvent(new Event("error"));
expect(slot?.classList.contains("is-fallback")).toBe(true);
expect(slot?.querySelector(".chat-avatar--sender-initials")?.textContent?.trim()).toBe("B");
});
});
describe("refreshChatAvatar", () => {
+37 -24
View File
@@ -12,6 +12,7 @@ import {
identityAvatarClass,
renderIdentityAvatarImage,
resolveIdentityAvatarView,
type IdentityAvatarView,
} from "../../components/identity-avatar-view.ts";
import type { AssistantIdentity } from "../../lib/assistant-identity.ts";
import {
@@ -22,6 +23,7 @@ import {
import { normalizeRoleForGrouping } from "../../lib/chat/message-normalizer.ts";
import type { SenderIdentity } from "../../lib/chat/sender-label.ts";
import { formatSenderLabel } from "../../lib/chat/sender-label.ts";
import { resolveAvatarInitials } from "../../lib/identity-avatar.ts";
import {
DEFAULT_AGENT_ID,
isUiGlobalSessionKey,
@@ -41,29 +43,7 @@ export function renderChatAvatar(
// Attributed multi-user messages show the author's own avatar (profile
// upload → gateway Gravatar proxy → initials), not the local viewer's.
if (normalized === "user" && sender) {
const label = formatSenderLabel(sender) ?? "";
const view = resolveIdentityAvatarView(sender);
const initialsAvatar = html`<div
class="chat-avatar user chat-avatar--sender-initials"
style=${`background: hsl(${view.fallback.colorSeed % 360} 48% 42%)`}
aria-label="${label}"
>
${view.fallback.initials}
</div>`;
if (!view.imageUrl) {
return initialsAvatar;
}
// The derived route may 404 (no upload, no Gravatar); swap to initials
// instead of a broken image. Lit reuses DOM parts, so a load must clear a
// prior sender's error state.
return html`<span class=${identityAvatarClass("chat-avatar-slot", view)}>
${renderIdentityAvatarImage({
view,
fallbackSelector: ".chat-avatar-slot",
className: "chat-avatar user",
alt: label,
})}${initialsAvatar}
</span>`;
return renderUserAvatarSlot(resolveIdentityAvatarView(sender), formatSenderLabel(sender) ?? "");
}
const assistantName = assistant?.name?.trim() || "Assistant";
const assistantAvatar = assistant?.avatar?.trim() || "";
@@ -119,7 +99,14 @@ export function renderChatAvatar(
: "other";
if (normalized === "user" && userAvatarUrl) {
return html`<img class="chat-avatar ${className}" src="${userAvatarUrl}" alt="${userName}" />`;
return renderUserAvatarSlot(
{
fallback: resolveAvatarInitials({ name: userName }),
imageUrl: userAvatarUrl,
pending: false,
},
userName,
);
}
if (normalized === "user" && userAvatarText) {
@@ -166,6 +153,32 @@ export function renderChatAvatar(
return html`<div class="chat-avatar ${className}">${initial}</div>`;
}
/**
* The avatar URL may 404 or be unreachable (missing upload, dead Gravatar,
* stale configured URL); swap to initials instead of a broken image. Lit
* reuses DOM parts, so a load must clear a prior identity's error state.
*/
function renderUserAvatarSlot(view: IdentityAvatarView, label: string) {
const initialsAvatar = html`<div
class="chat-avatar user chat-avatar--sender-initials"
style=${`background: hsl(${view.fallback.colorSeed % 360} 48% 42%)`}
aria-label="${label}"
>
${view.fallback.initials}
</div>`;
if (!view.imageUrl) {
return initialsAvatar;
}
return html`<span class=${identityAvatarClass("chat-avatar-slot", view)}>
${renderIdentityAvatarImage({
view,
fallbackSelector: ".chat-avatar-slot",
className: "chat-avatar user",
alt: label,
})}${initialsAvatar}
</span>`;
}
function isAvatarUrl(value: string): boolean {
const trimmed = value.trim();
return trimmed.startsWith("blob:") || isRenderableControlUiAvatarUrl(trimmed);
+1 -1
View File
@@ -177,7 +177,7 @@ class CustodianSurface extends OpenClawLightDomElement {
${store.sending
? html`<div class="chat-group assistant custodian__thinking-row" role="status">
<div class="chat-avatar assistant custodian__mascot-avatar" aria-hidden="true">
<openclaw-mascot mood="thinking" .size=${32}></openclaw-mascot>
<openclaw-mascot mood="thinking" .size=${26}></openclaw-mascot>
</div>
<div class="chat-group-messages custodian__thinking">
<span></span><span></span><span></span>
+7
View File
@@ -160,6 +160,13 @@
cursor: pointer;
}
.settings-section__help-button > span {
/* SF's ascent+descent exceed 1em, so a centered line box parks the glyph
below the circle's midline; trim to cap bounds for true centering. */
display: block;
text-box: trim-both cap alphabetic;
}
.settings-section__help-button:hover,
.settings-section__help-button:focus-visible {
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));