1import { type CSSProperties, memo } from "react";
2
3import { providerInstanceInitials } from "@t3tools/client-runtime/state/provider-instance-display";
4
5import { ProviderDriverKind } from "@t3tools/contracts";
6import { AntigravityIcon, ClaudeAI, Icon, OpenAI } from "../Icons";
7
8import { cn } from "~/lib/utils";
9import { providerClients } from "../settings/providerDriverMeta";
10import { ProviderPackageIcon } from "./ProviderPackageIcon";
11import {
12 AcpRegistryAgentIcon,
13 officialAcpRegistryIconUrlForAgentId,
14 resolveOfficialAcpRegistryIconUrl,
15} from "../settings/AcpRegistryIcon";
16
17const PROVIDER_ICON_BY_PROVIDER: Partial<Record<ProviderDriverKind, Icon>> = {
18 [ProviderDriverKind.make("codex")]: OpenAI,
19 [ProviderDriverKind.make("claudeAgent")]: ClaudeAI,
20 [ProviderDriverKind.make("antigravity")]: AntigravityIcon,
21};
22
23const PROVIDER_TEXT_COLOR_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = {
24 [ProviderDriverKind.make("codex")]: "text-black dark:text-white",
25 [ProviderDriverKind.make("claudeAgent")]: "text-[#d97757]",
26 [ProviderDriverKind.make("antigravity")]: "text-[#5b87bf]",
27};
28
29/** Brand text color for a provider label; package glyphs supply theirs as CSS variables. */
30export function providerTextColor(driverKind: ProviderDriverKind): {
31 readonly className?: string;
32 readonly style?: CSSProperties;
33} {
34 const icon = providerClients.get(driverKind)?.icon;
35 if (icon) {
36 return {
37 className: "text-(--icon-light) dark:text-(--icon-dark)",
38 style: { "--icon-light": icon.fill.light, "--icon-dark": icon.fill.dark } as CSSProperties,
39 };
40 }
41 const className = PROVIDER_TEXT_COLOR_BY_PROVIDER[driverKind];
42 return className ? { className } : {};
43}
44
45export function resolveProviderInstanceAcpRegistryIconUrl(input: {
46 readonly driverKind: ProviderDriverKind;
47 readonly agentId?: string | undefined;
48 readonly iconUrl?: string | undefined;
49}): string | null {
50 if (input.driverKind !== "acpRegistry") return null;
51 return (
52 resolveOfficialAcpRegistryIconUrl(input.iconUrl ?? null) ??
53 officialAcpRegistryIconUrlForAgentId(input.agentId?.trim() || null)
54 );
55}
56
57export const ProviderInstanceIcon = memo(function ProviderInstanceIcon(props: {
58 driverKind: ProviderDriverKind;
59 displayName: string;
60 accentColor?: string | undefined;
61 acpRegistryAgentId?: string | undefined;
62 acpRegistryIconUrl?: string | undefined;
63 showBadge?: boolean;
64 badgeContent?: "initials" | "none";
65 className?: string;
66 iconClassName?: string;
67 badgeClassName?: string;
68 statusDotClassName?: string;
69 indicatorBackground?: string;
70}) {
71 const Icon = PROVIDER_ICON_BY_PROVIDER[props.driverKind] ?? null;
72 const packageIcon = providerClients.get(props.driverKind)?.icon;
73 const indicatorBackground = props.indicatorBackground ?? "var(--card)";
74 const accentStyle = props.accentColor
75 ? ({ "--provider-accent": props.accentColor } as CSSProperties)
76 : undefined;
77 const badgeContent = props.badgeContent ?? "initials";
78 const isAcpRegistry = props.driverKind === "acpRegistry";
79 const acpRegistryIconUrl = resolveProviderInstanceAcpRegistryIconUrl({
80 driverKind: props.driverKind,
81 agentId: props.acpRegistryAgentId,
82 iconUrl: props.acpRegistryIconUrl,
83 });
84
85 return (
86 <span
87 className={cn(
88 "relative isolate z-30 inline-flex shrink-0 items-center justify-center overflow-visible",
89 props.className,
90 )}
91 style={accentStyle}
92 data-provider-accent-color={props.accentColor}
93 >
94 {isAcpRegistry ? (
95 <AcpRegistryAgentIcon
96 // The search-tile radius would crop most of the glyph at these
97 // inline sizes.
98 className={cn("size-5 rounded-none bg-transparent", props.iconClassName)}
99 fallbackClassName="size-full"
100 icon={acpRegistryIconUrl}
101 />
102 ) : packageIcon ? (
103 <ProviderPackageIcon
104 icon={packageIcon}
105 className={cn("size-5 shrink-0", props.iconClassName)}
106 aria-hidden
107 />
108 ) : Icon ? (
109 <Icon className={cn("size-5 shrink-0", props.iconClassName)} aria-hidden />
110 ) : (
111 <span className={cn("text-3xs font-semibold leading-none", props.iconClassName)}>
112 {providerInstanceInitials(props.displayName)}
113 </span>
114 )}
115 {props.statusDotClassName ? (
116 <span
117 className={cn(
118 "pointer-events-none absolute -left-0.5 -top-0.5 z-10 size-2 rounded-full",
119 props.statusDotClassName,
120 )}
121 style={{ boxShadow: `0 0 0 2px ${indicatorBackground}` }}
122 aria-hidden
123 />
124 ) : null}
125 {props.showBadge ? (
126 <span
127 className={cn(
128 "pointer-events-none absolute right-0 bottom-0 z-10 flex h-3.5 min-w-3.5 items-center justify-center rounded-full border px-0.5 text-4xs font-semibold leading-none shadow-sm",
129 props.accentColor
130 ? "bg-(--provider-accent) text-white"
131 : "bg-card text-muted-foreground",
132 props.badgeClassName,
133 )}
134 style={{ borderColor: indicatorBackground }}
135 aria-hidden
136 >
137 {badgeContent === "initials" ? providerInstanceInitials(props.displayName) : null}
138 </span>
139 ) : null}
140 </span>
141 );
142});