Skip to content
Source
Sign inSign up

providerInstanceDisplay.ts

92 lines3.3 KB

1import {
2 defaultInstanceIdForDriver,
3 PROVIDER_DISPLAY_NAMES,
4 type ProviderDriverKind,
5 type ServerProvider,
6} from "@t3tools/contracts";
7​
8function humanizeSlug(slug: string): string {
9 return slug
10 .replace(/([a-z])([A-Z])/g, "$1 $2")
11 .replace(/[_-]+/g, " ")
12 .trim()
13 .replace(/\b\w/g, (char) => char.toUpperCase());
15​
16export function resolveProviderInstanceDisplayName(
17 snapshot: Pick<ServerProvider, "instanceId" | "driver" | "displayName">,
18): string {
19 const trimmedSnapshotName = snapshot.displayName?.trim();
20 const kindLabel = PROVIDER_DISPLAY_NAMES[snapshot.driver] ?? humanizeSlug(snapshot.driver);
21 if (trimmedSnapshotName && trimmedSnapshotName !== kindLabel) return trimmedSnapshotName;
22 if (snapshot.instanceId !== defaultInstanceIdForDriver(snapshot.driver)) {
23 const humanized = humanizeSlug(snapshot.instanceId);
24 if (humanized.length > 0) return humanized;
25 }
26 return trimmedSnapshotName || kindLabel;
28​
29export function providerInstanceInitials(label: string): string {
30 // Punctuation separates words so "Codex (hub)" reads CH, not C(. An apostrophe
31 // inside a word stays ("Theo's Codex" is TC); a quoting one does not
32 // ("Codex 'hub'" is CH).
33 const words = label
34 .replace(/[^\P{P}'’]+/gu, " ")
35 .split(/\s+/u)
36 .map((word) => word.replace(/^['’]+|['’]+$/gu, ""))
37 .filter(Boolean);
38 if (words.length === 0) return "";
39 if (words.length === 1) return Array.from(words[0]!).slice(0, 2).join("").toUpperCase();
40 return words
41 .slice(0, 2)
42 .map((word) => Array.from(word)[0]?.toUpperCase() ?? "")
43 .join("");
45​
46export function normalizeProviderAccentColor(value: string | undefined): string | undefined {
47 const trimmed = value?.trim();
48 if (!trimmed) return undefined;
49 return /^#[0-9a-fA-F]{6}$/u.test(trimmed) ? trimmed : undefined;
51​
52export function providerAccentForegroundColor(accentColor: string) {
53 const linearChannel = (offset: number) => {
54 const value = Number.parseInt(accentColor.slice(offset, offset + 2), 16) / 255;
55 return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
56 };
57 const luminance =
58 0.2126 * linearChannel(1) + 0.7152 * linearChannel(3) + 0.0722 * linearChannel(5);
59 const blackContrast = (luminance + 0.05) / 0.05;
60 const whiteContrast = 1.05 / (luminance + 0.05);
61 return blackContrast >= whiteContrast ? "#000000" : "#ffffff";
63​
64export function shouldShowInstanceBadge(
65 entry: {
66 readonly driverKind: ProviderDriverKind;
67 readonly accentColor?: string | undefined;
68 readonly acpRegistryAgentId?: string | undefined;
69 readonly enabled: boolean;
70 },
71 entries: Iterable<{
72 readonly driverKind: ProviderDriverKind;
73 readonly acpRegistryAgentId?: string | undefined;
74 readonly enabled: boolean;
75 }>,
76): boolean {
77 if (normalizeProviderAccentColor(entry.accentColor)) return true;
78 let sharedProviderCount = 0;
79 for (const candidate of entries) {
80 if (candidate.driverKind !== entry.driverKind) continue;
81 // Historical threads still need to distinguish accounts that are all disabled.
82 if (entry.enabled && !candidate.enabled) continue;
83 if (
84 entry.driverKind === "acpRegistry" &&
85 candidate.acpRegistryAgentId !== entry.acpRegistryAgentId
86 ) {
87 continue;
88 }
89 if (++sharedProviderCount > 1) return true;
90 }
91 return false;