Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions apps/ui/src/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, test } from "bun:test";
import { avatarInitial } from "./constants";

describe("avatarInitial", () => {
test("uppercases a plain handle's first letter", () => {
expect(avatarInitial("Mintaka")).toBe("M");
});

test("uppercases a lowercase handle", () => {
expect(avatarInitial("rigel")).toBe("R");
});

test("empty string falls back to ?", () => {
expect(avatarInitial("")).toBe("?");
});

test("whitespace-only falls back to ?", () => {
expect(avatarInitial(" ")).toBe("?");
});

// An emoji carries no Latin letter, so it clamps to ? on its own merits.
test("emoji-leading handle falls back to ?", () => {
expect(avatarInitial("🚀ocket")).toBe("?");
});

// Guards the grapheme read: this astral char NFKD-folds to plain "A", so a
// regression to .at(0) would split the surrogate pair and return ? instead.
test("astral first character is read whole, not as half a surrogate", () => {
expect(avatarInitial("𝐀lpha")).toBe("A");
});

test("accented Latin handle strips the diacritic", () => {
expect(avatarInitial("Émile")).toBe("E");
});

test("non-Latin script (Cyrillic) falls back to ?", () => {
expect(avatarInitial("Живко")).toBe("?");
});

// ß uppercases to "SS"; we keep the FIRST resulting char, not ?, so a real
// letter still distinguishes the agent.
test("uppercase-expanding character keeps its first char", () => {
expect(avatarInitial("ßravo")).toBe("S");
});

// A digit is a printable ASCII char and survives the clamp — a handle like
// "3pio" tabs as "3", which tells it apart better than ?.
test("digit-leading handle keeps the digit", () => {
expect(avatarInitial("3pio")).toBe("3");
});

// Punctuation is likewise printable ASCII and kept — the derivation only
// falls back to ? for non-ASCII-representable scripts, not for ASCII symbols.
test("punctuation-leading handle keeps the punctuation", () => {
expect(avatarInitial("_hidden")).toBe("_");
});
});
19 changes: 17 additions & 2 deletions apps/ui/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,21 @@ export const RIGHT_SIDEBAR_TAB_BY_ID: {
export const RIGHT_SIDEBAR_ISSUE_ITEMS: readonly ActivityBarItem[] =
Object.values(RIGHT_SIDEBAR_TAB_BY_ID).filter((t) => t.group === "issue");

/** Derive an agent's activity-bar avatar initial from its handle, per D1 of
* design compass-glyph-primitives. Handles are charset-unconstrained (proto
* `from_handle`, no schema validation), so both activity-bar constructors
* derive through here — and the ASCII clamp is what lets the Unifont pin
* retire. An uppercase that expands (`ß`→`SS`) keeps the first letter rather
* than `?`, since the initial exists to tell agents apart; the tab's title
* carries the full handle either way. */
export function avatarInitial(handle: string): string {
const first = Array.from(handle.trim())[0];
if (first === undefined) return "?";
const folded = first.normalize("NFKD").replace(/\p{M}/gu, "").toUpperCase();
const ascii = Array.from(folded)[0];
return ascii !== undefined && /^[\x21-\x7e]$/.test(ascii) ? ascii : "?";
}

/** Build the fleet activity-bar item for a RESOLVABLE pinned agent (Record A
* §T2; RIG-1645 P1). The tab id is the `agent:`-prefixed account id (the open
* arm of `RightSidebarTab`); the icon is the agent handle's initial (matching
Expand All @@ -134,7 +149,7 @@ export const RIGHT_SIDEBAR_ISSUE_ITEMS: readonly ActivityBarItem[] =
export function fleetItemForAgent(agent: Agent): ActivityBarItem {
return {
id: `agent:${agent.account.id}`,
icon: (agent.account.handle.at(0) ?? "?").toUpperCase(),
icon: avatarInitial(agent.account.handle),
title: agent.account.handle,
group: "fleet",
agentId: agent.account.id,
Expand All @@ -151,7 +166,7 @@ export function fleetItemForAgent(agent: Agent): ActivityBarItem {
export function unreachableFleetItem(pin: PinnedAgent): ActivityBarItem {
return {
id: `agent:${pin.id}`,
icon: (pin.handle.at(0) ?? "?").toUpperCase(),
icon: avatarInitial(pin.handle),
title: pin.handle,
group: "fleet",
agentId: pin.id,
Expand Down
Loading