diff --git a/ui/src/ui/views/agents-utils.test.ts b/ui/src/ui/views/agents-utils.test.ts index a9b30e549db..daa909e2b2c 100644 --- a/ui/src/ui/views/agents-utils.test.ts +++ b/ui/src/ui/views/agents-utils.test.ts @@ -3,6 +3,7 @@ import { agentLogoUrl, resolveConfiguredCronModelSuggestions, resolveAgentAvatarUrl, + resolveAgentEmoji, resolveEffectiveModelFallbacks, sortLocaleStrings, } from "./agents-utils.ts"; @@ -131,3 +132,17 @@ describe("resolveAgentAvatarUrl", () => { expect(resolveAgentAvatarUrl({ identity: { avatar: "🦞" } })).toBeNull(); }); }); + +describe("resolveAgentEmoji", () => { + it("strips bidi controls from emoji values", () => { + expect(resolveAgentEmoji({ identity: { emoji: "\u202E🤖" } })).toBe("🤖"); + }); + + it("keeps only the first grapheme cluster", () => { + expect(resolveAgentEmoji({ identity: { emoji: "🤖😈" } })).toBe("🤖"); + }); + + it("falls back to empty for control-only emoji strings", () => { + expect(resolveAgentEmoji({ identity: { emoji: "\u202E\u2066" } })).toBe(""); + }); +}); diff --git a/ui/src/ui/views/agents-utils.ts b/ui/src/ui/views/agents-utils.ts index 8245438f1ff..33734505cb9 100644 --- a/ui/src/ui/views/agents-utils.ts +++ b/ui/src/ui/views/agents-utils.ts @@ -221,6 +221,23 @@ export function agentLogoUrl(basePath: string): string { return base ? `${base}/favicon.svg` : "favicon.svg"; } +const EMOJI_CONTROL_CHARS_RE = /[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g; + +function sanitizeEmojiValue(value: string): string { + const cleaned = value.replaceAll(EMOJI_CONTROL_CHARS_RE, "").trim(); + if (!cleaned) { + return ""; + } + if (typeof Intl.Segmenter !== "function") { + return cleaned; + } + const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); + const first = segmenter.segment(cleaned)[Symbol.iterator]().next().value as + | { segment?: string } + | undefined; + return first?.segment?.trim() || ""; +} + function isLikelyEmoji(value: string) { const trimmed = value.trim(); if (!trimmed) { @@ -245,24 +262,35 @@ function isLikelyEmoji(value: string) { return true; } +function pickEmoji(value?: string | null): string { + if (!value) { + return ""; + } + const sanitized = sanitizeEmojiValue(value); + if (!sanitized) { + return ""; + } + return isLikelyEmoji(sanitized) ? sanitized : ""; +} + export function resolveAgentEmoji( agent: { identity?: { emoji?: string; avatar?: string } }, agentIdentity?: AgentIdentityResult | null, ) { - const identityEmoji = agentIdentity?.emoji?.trim(); - if (identityEmoji && isLikelyEmoji(identityEmoji)) { + const identityEmoji = pickEmoji(agentIdentity?.emoji); + if (identityEmoji) { return identityEmoji; } - const agentEmoji = agent.identity?.emoji?.trim(); - if (agentEmoji && isLikelyEmoji(agentEmoji)) { + const agentEmoji = pickEmoji(agent.identity?.emoji); + if (agentEmoji) { return agentEmoji; } - const identityAvatar = agentIdentity?.avatar?.trim(); - if (identityAvatar && isLikelyEmoji(identityAvatar)) { + const identityAvatar = pickEmoji(agentIdentity?.avatar); + if (identityAvatar) { return identityAvatar; } - const avatar = agent.identity?.avatar?.trim(); - if (avatar && isLikelyEmoji(avatar)) { + const avatar = pickEmoji(agent.identity?.avatar); + if (avatar) { return avatar; } return "";