diff --git a/ui/src/ui/views/agents-utils.test.ts b/ui/src/ui/views/agents-utils.test.ts
index a9b30e549db..958f5ece7e3 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,21 @@ 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("");
+ });
+
+ it("preserves ZWJ-composed compound emoji intact", () => {
+ expect(resolveAgentEmoji({ identity: { emoji: "👩💻" } })).toBe("👩💻");
+ });
+});
diff --git a/ui/src/ui/views/agents-utils.ts b/ui/src/ui/views/agents-utils.ts
index e0c06c41386..fd3e018e330 100644
--- a/ui/src/ui/views/agents-utils.ts
+++ b/ui/src/ui/views/agents-utils.ts
@@ -194,7 +194,7 @@ export function normalizeAgentLabel(agent: {
return agent.name?.trim() || agent.identity?.name?.trim() || agent.id;
}
-const AVATAR_URL_RE = /^(https?:\/\/|data:image\/|\/)/i;
+const AVATAR_URL_RE = /^(https?:\/\/|data:image\/|blob:|\/)/i;
export function resolveAgentAvatarUrl(
agent: { identity?: { avatar?: string; avatarUrl?: string } },
@@ -221,6 +221,23 @@ export function agentLogoUrl(basePath: string): string {
return base ? `${base}/favicon.svg` : "favicon.svg";
}
+const EMOJI_CONTROL_CHARS_RE = /[\u200B\u200C\u200E\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 "";
diff --git a/ui/src/ui/views/agents.ts b/ui/src/ui/views/agents.ts
index 4e8b9a065ba..680123090cf 100644
--- a/ui/src/ui/views/agents.ts
+++ b/ui/src/ui/views/agents.ts
@@ -16,7 +16,12 @@ import {
renderAgentCron,
} from "./agents-panels-status-files.ts";
import { renderAgentTools, renderAgentSkills } from "./agents-panels-tools-skills.ts";
-import { agentBadgeText, buildAgentContext, normalizeAgentLabel } from "./agents-utils.ts";
+import {
+ agentBadgeText,
+ buildAgentContext,
+ normalizeAgentLabel,
+ resolveAgentEmoji,
+} from "./agents-utils.ts";
export type AgentsPanel = "overview" | "files" | "tools" | "skills" | "channels" | "cron";
@@ -149,13 +154,19 @@ export function renderAgents(props: AgentsProps) {
? html`
`
- : agents.map(
- (agent) => html`
+ : agents.map((agent) => {
+ const emoji = resolveAgentEmoji(
+ agent,
+ props.agentIdentityById[agent.id] ?? null,
+ );
+ const label = normalizeAgentLabel(agent);
+ const badge = agentBadgeText(agent.id, defaultId);
+ return html`
- `,
- )
+ `;
+ })
}