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
49 changes: 48 additions & 1 deletion frontend/src/v2/__tests__/podRecency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,54 @@ describe('podRecency', () => {
test('podInitials takes two words, or two letters of one', () => {
expect(podInitials('Connectors v2')).toBe('CV');
expect(podInitials('Sharpen')).toBe('SH');
expect(podInitials(' Payments — memory demo ')).toBe('P—');
expect(podInitials('')).toBe('·');
});

test('podInitials separates on punctuation and dashes instead of taking them as initials', () => {
expect(podInitials(' Payments — memory demo ')).toBe('PM');
expect(podInitials('Sharpen — pod model')).toBe('SP');
expect(podInitials('Team-Commonly')).toBe('TC');
expect(podInitials('—')).toBe('·');
});

test('podInitials shows one glyph when the picked glyph is CJK', () => {
expect(podInitials('设计评审')).toBe('设');
expect(podInitials('Growth 团队')).toBe('G');
});

test('podInitials cuts every CJK script, not just Han', () => {
// Han alone carried the predicate until these existed: dropping any ONE of
// the other three scripts from the class left the suite green, so a later
// "simplification" to a single script would have gone unnoticed.
expect(podInitials('ひらがな')).toBe('ひ');
expect(podInitials('カタカナ')).toBe('カ');
// Two words on purpose, so first() and the CJK branch are exercised together.
expect(podInitials('한국어 팀')).toBe('한');
});

test('podInitials states its scope: a fullwidth-Latin pick is not CJK', () => {
// The predicate is a script list and JS \p{…} cannot express
// East_Asian_Width, so fullwidth Latin shows both glyphs. Asserted rather
// than left in a comment — a known miss a test pins is a decision, and this
// assertion belongs in its own test so widening the class relocates it
// instead of tripping the CJK test.
expect(podInitials('ABC')).toBe('AB');
});

test('podInitials drops an emoji the way it drops punctuation', () => {
expect(podInitials('🚀 Launch')).toBe('LA');
});

test('podInitials slices by code point, so a surrogate pair is never cut in half', () => {
// Synthetic on purpose: a two-unit slice of 'A𝔘' ends between the halves of
// 𝔘 and returns a replacement character, which no real pod name would show.
expect(podInitials('A𝔘')).toBe('A𝔘');
});

test('podInitials reads the first code point of each of two words', () => {
// Pins the two-word branch's own code-point read. 'A𝔘' above is a single
// token, so it only exercises the one-word slice; a UTF-16 read here would
// cut 𐐀 in half and return a replacement character.
expect(podInitials('𐐀lpha Beta')).toBe('𐐀B');
});
});
37 changes: 34 additions & 3 deletions frontend/src/v2/lib/podRecency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,42 @@ export const relativeTime = (value: string | number | Date | null | undefined, n
return `${Math.floor(delta / YEAR)}y`;
};

// The mark is 22px with 11px mono characters, so two Han characters wrap to a
// second line inside the square — that is why a CJK pick is cut to one glyph.
//
// This is a SCRIPT list, so the rule it states is a CJK rule and not a width
// rule. JS `\p{…}` expresses General_Category, Script and binary properties but
// not East_Asian_Width, so `AB` (fullwidth Latin, EAW=Fullwidth) is out of
// scope by decision and shows both glyphs. The 22px sentence above is the
// motivation for the rule, not a contract this predicate can compute.
const CJK_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;

// Two-letter mark for a pod without an avatar: initials of the first two words,
// or the first two letters of a one-word name.
//
// Tokenising matches `initialsFor`: the same split, the same [^\p{L}\p{N}]
// strip and the same filter, so punctuation and dashes separate words instead
// of becoming initials and "Sharpen — pod model" is SP rather than S—. It does
// not copy initialsFor's parenthetical drop, deliberately: "(v2)" distinguishes
// a pod name even though "(lead)" never distinguishes an agent. An emoji drops
// out the way punctuation does, which is why "🚀 Launch" is LA rather than 🚀L. A
// name whose picked glyphs are CJK shows one glyph — "设计评审" is 设, not the
// two characters that would wrap. Code points rather than UTF-16 units
// throughout, so a surrogate pair is never cut in half.
export const podInitials = (name: string): string => {
const words = (name || '').trim().split(/\s+/).filter(Boolean);
const words = String(name || '')
.trim()
.split(/[\s_\-/|]+/)
.map((word) => word.replace(/[^\p{L}\p{N}]/gu, ''))
.filter(Boolean);
if (words.length === 0) return '·';
if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
return (words[0][0] + words[1][0]).toUpperCase();

const first = (word: string): string => Array.from(word)[0] || '';
const pick = words.length === 1
? Array.from(words[0]).slice(0, 2).join('')
: first(words[0]) + first(words[1]);

const picked = Array.from(pick);
if (picked.some((char) => CJK_SCRIPT.test(char))) return picked[0];
return pick.toUpperCase();
};
Loading