Skip to content

Commit 8b06ca2

Browse files
hannasdevaltaywtf
andauthored
fix(avatar): check ui.assistant.avatar in resolveAvatarSource (openclaw#60778)
Merged via squash. Prepared head SHA: df8d953 Co-authored-by: hannasdev <4538260+hannasdev@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf
1 parent 63cabcb commit 8b06ca2

4 files changed

Lines changed: 90 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Docs: https://docs.openclaw.ai
110110
- Agents/exec: restore `host=node` routing for node-pinned and `host=auto` sessions, while still blocking sandboxed `auto` sessions from jumping to gateway. (#60788) Thanks @openperf.
111111
- Agents/compaction: keep assistant tool calls and displaced tool results in the same compaction chunk so strict summarization providers stop rejecting orphaned tool pairs. (#58849) Thanks @openperf.
112112
- Outbound/sanitizer: strip leaked `<tool_call>`, `<function_calls>`, and model special tokens from shared user-visible assistant text, including truncated tool-call streams, so internal scaffolding no longer bleeds into replies across surfaces. (#60619) Thanks @oliviareid-svg.
113+
- Control UI/avatar: honor `ui.assistant.avatar` when serving `/avatar/:agentId` so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.
113114

114115
## 2026.4.2
115116

src/agents/identity-avatar.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ async function expectLocalAvatarPath(
1515
cfg: OpenClawConfig,
1616
workspace: string,
1717
expectedRelativePath: string,
18+
opts?: Parameters<typeof resolveAgentAvatar>[2],
1819
) {
1920
const workspaceReal = await fs.realpath(workspace);
20-
const resolved = resolveAgentAvatar(cfg, "main");
21+
const resolved = resolveAgentAvatar(cfg, "main", opts);
2122
expect(resolved.kind).toBe("local");
2223
if (resolved.kind === "local") {
2324
const resolvedReal = await fs.realpath(resolved.filePath);
@@ -164,4 +165,72 @@ describe("resolveAgentAvatar", () => {
164165
const data = resolveAgentAvatar(cfg, "data");
165166
expect(data.kind).toBe("data");
166167
});
168+
169+
it("resolves local avatar from ui.assistant.avatar when no agents.list identity is set", async () => {
170+
const root = await createTempAvatarRoot();
171+
const workspace = path.join(root, "work");
172+
const avatarPath = path.join(workspace, "ui-avatar.png");
173+
await writeFile(avatarPath);
174+
175+
const cfg: OpenClawConfig = {
176+
ui: { assistant: { avatar: "ui-avatar.png" } },
177+
agents: { list: [{ id: "main", workspace }] },
178+
};
179+
180+
await expectLocalAvatarPath(cfg, workspace, "ui-avatar.png", { includeUiOverride: true });
181+
});
182+
183+
it("ui.assistant.avatar ignored without includeUiOverride (outbound callers)", async () => {
184+
const root = await createTempAvatarRoot();
185+
const workspace = path.join(root, "work");
186+
const uiAvatarPath = path.join(workspace, "ui-avatar.png");
187+
const cfgAvatarPath = path.join(workspace, "cfg-avatar.png");
188+
await writeFile(uiAvatarPath);
189+
await writeFile(cfgAvatarPath);
190+
191+
const cfg: OpenClawConfig = {
192+
ui: { assistant: { avatar: "ui-avatar.png" } },
193+
agents: { list: [{ id: "main", workspace, identity: { avatar: "cfg-avatar.png" } }] },
194+
};
195+
196+
// Without the opt-in, outbound callers get the per-agent identity avatar, not the UI override.
197+
await expectLocalAvatarPath(cfg, workspace, "cfg-avatar.png");
198+
});
199+
200+
it("ui.assistant.avatar takes priority over agents.list identity.avatar with includeUiOverride", async () => {
201+
const root = await createTempAvatarRoot();
202+
const workspace = path.join(root, "work");
203+
const uiAvatarPath = path.join(workspace, "ui-avatar.png");
204+
const cfgAvatarPath = path.join(workspace, "cfg-avatar.png");
205+
await writeFile(uiAvatarPath);
206+
await writeFile(cfgAvatarPath);
207+
208+
const cfg: OpenClawConfig = {
209+
ui: { assistant: { avatar: "ui-avatar.png" } },
210+
agents: { list: [{ id: "main", workspace, identity: { avatar: "cfg-avatar.png" } }] },
211+
};
212+
213+
await expectLocalAvatarPath(cfg, workspace, "ui-avatar.png", { includeUiOverride: true });
214+
});
215+
216+
it("ui.assistant.avatar takes priority over IDENTITY.md avatar with includeUiOverride", async () => {
217+
const root = await createTempAvatarRoot();
218+
const workspace = path.join(root, "work");
219+
const uiAvatarPath = path.join(workspace, "ui-avatar.png");
220+
const identityAvatarPath = path.join(workspace, "identity-avatar.png");
221+
await writeFile(uiAvatarPath);
222+
await writeFile(identityAvatarPath);
223+
await fs.writeFile(
224+
path.join(workspace, "IDENTITY.md"),
225+
"- Avatar: identity-avatar.png\n",
226+
"utf-8",
227+
);
228+
229+
const cfg: OpenClawConfig = {
230+
ui: { assistant: { avatar: "ui-avatar.png" } },
231+
agents: { list: [{ id: "main", workspace }] },
232+
};
233+
234+
await expectLocalAvatarPath(cfg, workspace, "ui-avatar.png", { includeUiOverride: true });
235+
});
167236
});

src/agents/identity-avatar.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,17 @@ function normalizeAvatarValue(value: string | undefined | null): string | null {
2424
return trimmed ? trimmed : null;
2525
}
2626

27-
function resolveAvatarSource(cfg: OpenClawConfig, agentId: string): string | null {
27+
function resolveAvatarSource(
28+
cfg: OpenClawConfig,
29+
agentId: string,
30+
opts?: { includeUiOverride?: boolean },
31+
): string | null {
32+
if (opts?.includeUiOverride) {
33+
const fromUiConfig = normalizeAvatarValue(cfg.ui?.assistant?.avatar);
34+
if (fromUiConfig) {
35+
return fromUiConfig;
36+
}
37+
}
2838
const fromConfig = normalizeAvatarValue(resolveAgentIdentity(cfg, agentId)?.avatar);
2939
if (fromConfig) {
3040
return fromConfig;
@@ -73,8 +83,12 @@ function resolveLocalAvatarPath(params: {
7383
return { ok: true, filePath: realPath };
7484
}
7585

76-
export function resolveAgentAvatar(cfg: OpenClawConfig, agentId: string): AgentAvatarResolution {
77-
const source = resolveAvatarSource(cfg, agentId);
86+
export function resolveAgentAvatar(
87+
cfg: OpenClawConfig,
88+
agentId: string,
89+
opts?: { includeUiOverride?: boolean },
90+
): AgentAvatarResolution {
91+
const source = resolveAvatarSource(cfg, agentId, opts);
7892
if (!source) {
7993
return { kind: "none", reason: "missing" };
8094
}

src/gateway/server-http.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,8 @@ export function createGatewayHttpServer(opts: {
946946
run: () =>
947947
handleControlUiAvatarRequest(req, res, {
948948
basePath: controlUiBasePath,
949-
resolveAvatar: (agentId) => resolveAgentAvatar(configSnapshot, agentId),
949+
resolveAvatar: (agentId) =>
950+
resolveAgentAvatar(configSnapshot, agentId, { includeUiOverride: true }),
950951
}),
951952
});
952953
requestStages.push({

0 commit comments

Comments
 (0)