Merge dd874d52f6314e05c0a2078dd3b64caed8f61f0c into 9fb78453e088cd7b553d7779faa0de5c83708e70

This commit is contained in:
Marcus Widing 2026-03-20 22:19:37 -07:00 committed by GitHub
commit 879714ae79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 15 deletions

View File

@ -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("👩‍💻");
});
});

View File

@ -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 "";

View File

@ -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`
<option value="">No agents</option>
`
: 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`
<option value=${agent.id} ?selected=${agent.id === selectedId}>
${normalizeAgentLabel(agent)}${agentBadgeText(agent.id, defaultId) ? ` (${agentBadgeText(agent.id, defaultId)})` : ""}
${emoji ? `${emoji} ` : ""}${label}${badge ? ` (${badge})` : ""}
</option>
`,
)
`;
})
}
</select>
</div>