From 9d09a7879c2e52753e00ce89a3a00fb6c562a0ff Mon Sep 17 00:00:00 2001 From: Dave Lauer Date: Thu, 22 Jan 2026 12:09:27 -0500 Subject: [PATCH 1/2] fix(ui): allow relative URLs in avatar validation The isAvatarUrl check only accepted http://, https://, or data: URLs, but the /avatar/{agentId} endpoint returns relative paths like /avatar/main. This caused local file avatars to display as text instead of images. Fixes avatar display for locally configured avatar files. --- ui/src/ui/chat/grouped-render.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index 6a36c7898..408637082 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -158,7 +158,8 @@ function renderAvatar( function isAvatarUrl(value: string): boolean { return ( /^https?:\/\//i.test(value) || - /^data:image\//i.test(value) + /^data:image\//i.test(value) || + /^\//.test(value) // Relative paths from avatar endpoint ); } From ffca65d15fe0d77feefe6da7115f9d5a3dac35cd Mon Sep 17 00:00:00 2001 From: Dave Lauer Date: Thu, 22 Jan 2026 15:16:31 -0500 Subject: [PATCH 2/2] fix(gateway): resolve local avatars to URL in HTML injection and RPC The frontend fix alone wasn't enough because: 1. serveIndexHtml() was injecting the raw avatar filename into HTML 2. agent.identity.get RPC was returning raw filename, overwriting the HTML-injected value Now both paths resolve local file avatars (*.png, *.jpg, etc.) to the /avatar/{agentId} endpoint URL. --- src/gateway/control-ui.ts | 13 ++++++++++++- src/gateway/server-methods/agent.ts | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index 8d2d881b1..13ee309de 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -206,11 +206,22 @@ interface ServeIndexHtmlOpts { agentId?: string; } +function looksLikeLocalAvatarPath(value: string | undefined): boolean { + if (!value) return false; + if (/^https?:\/\//i.test(value) || /^data:image\//i.test(value)) return false; + return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value); +} + function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndexHtmlOpts) { const { basePath, config, agentId } = opts; const identity = config ? resolveAssistantIdentity({ cfg: config, agentId }) : DEFAULT_ASSISTANT_IDENTITY; + // Resolve local file avatars to /avatar/{agentId} URL + let avatarValue = identity.avatar; + if (looksLikeLocalAvatarPath(avatarValue) && identity.agentId) { + avatarValue = buildAvatarUrl(basePath, identity.agentId); + } res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Cache-Control", "no-cache"); const raw = fs.readFileSync(indexPath, "utf8"); @@ -218,7 +229,7 @@ function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndex injectControlUiConfig(raw, { basePath, assistantName: identity.name, - assistantAvatar: identity.avatar, + assistantAvatar: avatarValue, }), ); } diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index aaeb3c272..e99d0ecdf 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -407,7 +407,18 @@ export const agentHandlers: GatewayRequestHandlers = { } const cfg = loadConfig(); const identity = resolveAssistantIdentity({ cfg, agentId }); - respond(true, identity, undefined); + // Resolve local file avatars to /avatar/{agentId} URL + let avatarValue = identity.avatar; + if ( + avatarValue && + !/^https?:\/\//i.test(avatarValue) && + !/^data:image\//i.test(avatarValue) && + /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(avatarValue) && + identity.agentId + ) { + avatarValue = `/avatar/${identity.agentId}`; + } + respond(true, { ...identity, avatar: avatarValue }, undefined); }, "agent.wait": async ({ params, respond }) => { if (!validateAgentWaitParams(params)) {