| 1 | /** |
| 2 | * How a configured provider instance presents itself in a client: its label, |
| 3 | * its accent color, and whether its icon carries the account badge. Shared by |
| 4 | * web and mobile so both clients name and badge the same instance identically. |
| 5 | * |
| 6 | * @module providerInstanceDisplay |
| 7 | */ |
| 8 | import { |
| 9 | defaultInstanceIdForDriver, |
| 10 | PROVIDER_DISPLAY_NAMES, |
| 11 | type ProviderDriverKind, |
| 12 | type ServerProvider, |
| 13 | } from "@t3tools/contracts"; |
| 14 | |
| 15 | /** |
| 16 | * Title-case a slug: splits on `_` / `-` and camelCase boundaries, so |
| 17 | * `codex_personal` becomes "Codex Personal" and `myCustomInstance` becomes |
| 18 | * "My Custom Instance". |
| 19 | */ |
| 20 | function humanizeSlug(slug: string): string { |
| 21 | return slug |
| 22 | .replace(/([a-z])([A-Z])/g, "$1 $2") |
| 23 | .replace(/[_-]+/g, " ") |
| 24 | .trim() |
| 25 | .replace(/\b\w/g, (char) => char.toUpperCase()); |
| 26 | } |
| 27 | |
| 28 | /** |
| 29 | * Resolve an instance's label with a tiered priority: |
| 30 | * |
| 31 | * 1. A snapshot `displayName` that differs from the driver's brand label — |
| 32 | * the server has explicitly named this instance, trust it. |
| 33 | * 2. For non-default instances, a humanized `instanceId` — the server fell |
| 34 | * back to the driver-level label (the same for every instance of that |
| 35 | * kind), so the slug is what keeps "Codex" and "Codex Personal" apart. |
| 36 | * 3. The snapshot's `displayName`, or the brand label from contracts. |
| 37 | */ |
| 38 | export function resolveProviderInstanceDisplayName( |
| 39 | snapshot: Pick<ServerProvider, "instanceId" | "driver" | "displayName">, |
| 40 | ): string { |
| 41 | const trimmedSnapshotName = snapshot.displayName?.trim(); |
| 42 | const kindLabel = PROVIDER_DISPLAY_NAMES[snapshot.driver] ?? humanizeSlug(snapshot.driver); |
| 43 | if (trimmedSnapshotName && trimmedSnapshotName !== kindLabel) return trimmedSnapshotName; |
| 44 | if (snapshot.instanceId !== defaultInstanceIdForDriver(snapshot.driver)) { |
| 45 | const humanized = humanizeSlug(snapshot.instanceId); |
| 46 | if (humanized.length > 0) return humanized; |
| 47 | } |
| 48 | return trimmedSnapshotName || kindLabel; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Turn a display name into up to two initials for the badge: the first two |
| 53 | * characters of a single word, or the first character of each of the first |
| 54 | * two words. Iterates by code point so an emoji never splits into surrogates. |
| 55 | */ |
| 56 | export function providerInstanceInitials(label: string): string { |
| 57 | const words = label.replace(/[_-]+/g, " ").split(/\s+/u).filter(Boolean); |
| 58 | if (words.length === 0) return ""; |
| 59 | if (words.length === 1) return Array.from(words[0]!).slice(0, 2).join("").toUpperCase(); |
| 60 | return words |
| 61 | .slice(0, 2) |
| 62 | .map((word) => Array.from(word)[0]?.toUpperCase() ?? "") |
| 63 | .join(""); |
| 64 | } |
| 65 | |
| 66 | /** Only `#rrggbb` accent colors render; anything else is treated as unset. */ |
| 67 | export function normalizeProviderAccentColor(value: string | undefined): string | undefined { |
| 68 | const trimmed = value?.trim(); |
| 69 | if (!trimmed) return undefined; |
| 70 | return /^#[0-9a-fA-F]{6}$/u.test(trimmed) ? trimmed : undefined; |
| 71 | } |
| 72 | |
| 73 | /** |
| 74 | * Whether an instance's icon carries the account badge: accent color set, or |
| 75 | * several instances sharing a provider so the brand glyph alone is ambiguous. |
| 76 | * ACP agents have distinct glyphs even though they share the registry driver. |
| 77 | * Shared by the composer trigger, the picker rail, and sidebar/thread rows. |
| 78 | */ |
| 79 | export function shouldShowInstanceBadge( |
| 80 | entry: { |
| 81 | readonly driverKind: ProviderDriverKind; |
| 82 | readonly accentColor?: string | undefined; |
| 83 | readonly acpRegistryAgentId?: string | undefined; |
| 84 | }, |
| 85 | entries: Iterable<{ |
| 86 | readonly driverKind: ProviderDriverKind; |
| 87 | readonly acpRegistryAgentId?: string | undefined; |
| 88 | }>, |
| 89 | ): boolean { |
| 90 | if (entry.accentColor) return true; |
| 91 | let sharedProviderCount = 0; |
| 92 | for (const candidate of entries) { |
| 93 | if (candidate.driverKind !== entry.driverKind) continue; |
| 94 | if ( |
| 95 | entry.driverKind === "acpRegistry" && |
| 96 | candidate.acpRegistryAgentId !== entry.acpRegistryAgentId |
| 97 | ) { |
| 98 | continue; |
| 99 | } |
| 100 | if (++sharedProviderCount > 1) return true; |
| 101 | } |
| 102 | return false; |
| 103 | } |