Merge dd874d52f6314e05c0a2078dd3b64caed8f61f0c into 9fb78453e088cd7b553d7779faa0de5c83708e70
This commit is contained in:
commit
879714ae79
@ -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("👩💻");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 "";
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user