export const MENTION_CUSTOM_TYPE = 'chi.messaging.mentions'; export type HumanIdentity = { id: `github:${string}`; handle: string; }; export type MentionMarker = { schemaVersion: 1; actor: HumanIdentity; recipients: HumanIdentity[]; target: { sessionId: string; entryId: string }; }; const HANDLE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/; export function identityFromOwnerId(ownerId: string): HumanIdentity | null { const match = /^github:(.+)$/i.exec(ownerId.trim()); const handle = match?.[1] ?? ''; if (!HANDLE.test(handle)) return null; return { id: `github:${handle.toLowerCase()}`, handle }; } export function mentionHandles(text: string): string[] { const handles: string[] = []; const seen = new Set(); for (const match of text.matchAll(/(?:^|[\s(])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?)(?=$|[\s),.!?:;])/g)) { const handle = match[1]!; const key = handle.toLowerCase(); if (!seen.has(key)) handles.push(handle); seen.add(key); } return handles; } export function recipientsOf(text: string, participants: readonly HumanIdentity[]): HumanIdentity[] { const byHandle = new Map(participants.map((participant) => [participant.handle.toLowerCase(), participant])); return mentionHandles(text).flatMap((handle) => { const participant = byHandle.get(handle.toLowerCase()); return participant ? [participant] : []; }); } export function markerFor(actor: HumanIdentity, recipients: readonly HumanIdentity[], sessionId: string, entryId: string): MentionMarker { return { schemaVersion: 1, actor, recipients: [...recipients].sort((left, right) => left.id.localeCompare(right.id)), target: { sessionId, entryId }, }; }