feat(FN-2590): add xAI and opencode provider icon support
- Add dedicated xAI and Opencode SVG icon components in ProviderIcon with provider config entries - Map grok to the xAI icon and add opencode color token for consistent themed rendering - Extend UsageIndicator provider normalization to resolve xai/grok and opencode icon keys - Add ProviderIcon tests for xai, grok alias behavior, and opencode rendering/color normalization
This commit is contained in:
@@ -155,6 +155,44 @@ function KimiIcon({ size, color, label = "Kimi" }: { size: number; color: string
|
||||
);
|
||||
}
|
||||
|
||||
function XaiIcon({ size, color, label = "xAI" }: { size: number; color: string; label?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="xai-icon"
|
||||
aria-label={label}
|
||||
>
|
||||
<path
|
||||
d="M2.21 3l7.964 11.386L2.13 21h1.974l6.843-7.07L17.165 21H22.1l-8.392-11.97L21.17 3h-1.974l-6.349 6.56L6.845 3z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function OpencodeIcon({ size, color, label = "Opencode" }: { size: number; color: string; label?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="opencode-icon"
|
||||
aria-label={label}
|
||||
>
|
||||
<path
|
||||
d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// OpenRouter ring mark — simplified geometric version of the OpenRouter brand symbol.
|
||||
function OpenRouterIcon({ size, color, label = "OpenRouter" }: { size: number; color: string; label?: string }) {
|
||||
return (
|
||||
@@ -258,6 +296,11 @@ const providerConfig: Record<
|
||||
kimi: { component: KimiIcon, color: "var(--provider-kimi)" },
|
||||
moonshot: { component: KimiIcon, color: "var(--provider-kimi)" }, // Moonshot alias
|
||||
"kimi-coding": { component: KimiIcon, color: "var(--provider-kimi)", label: "Kimi" }, // Kimi alias
|
||||
|
||||
xai: { component: XaiIcon, color: "var(--text)" },
|
||||
grok: { component: XaiIcon, color: "var(--text)", label: "xAI" },
|
||||
|
||||
opencode: { component: OpencodeIcon, color: "var(--provider-opencode)" },
|
||||
};
|
||||
|
||||
export function ProviderIcon({ provider, size = "sm" }: ProviderIconProps) {
|
||||
|
||||
@@ -238,7 +238,13 @@ function getProviderIconKey(providerName: string): string {
|
||||
if (normalized.includes('kimi') || normalized.includes('moonshot')) {
|
||||
return 'kimi';
|
||||
}
|
||||
|
||||
if (normalized.includes('xai') || normalized.includes('grok')) {
|
||||
return 'xai';
|
||||
}
|
||||
if (normalized.includes('opencode')) {
|
||||
return 'opencode';
|
||||
}
|
||||
|
||||
// Return the original name as fallback (ProviderIcon will show a default icon)
|
||||
return providerName;
|
||||
}
|
||||
|
||||
@@ -355,6 +355,74 @@ describe("ProviderIcon", () => {
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-kimi)");
|
||||
});
|
||||
|
||||
// xAI provider tests
|
||||
it("renders xAI brand icon for xai provider", () => {
|
||||
render(<ProviderIcon provider="xai" />);
|
||||
expect(screen.getByTestId("xai-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("xAI")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies theme-safe color for xai", () => {
|
||||
render(<ProviderIcon provider="xai" />);
|
||||
const icon = screen.getByTestId("xai-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "var(--text)" });
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for xai", () => {
|
||||
render(<ProviderIcon provider="xai" />);
|
||||
const svg = screen.getByTestId("xai-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--text)");
|
||||
});
|
||||
|
||||
it("normalizes Xai (capitalized) to xai", () => {
|
||||
render(<ProviderIcon provider="Xai" />);
|
||||
expect(screen.getByTestId("xai-icon")).toBeInTheDocument();
|
||||
const wrapper = screen.getByTestId("xai-icon").parentElement;
|
||||
expect(wrapper).toHaveAttribute("data-provider", "xai");
|
||||
});
|
||||
|
||||
it("renders xAI brand icon for grok provider (alias)", () => {
|
||||
render(<ProviderIcon provider="grok" />);
|
||||
expect(screen.getByTestId("xai-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("xAI")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies theme-safe color for grok (alias)", () => {
|
||||
render(<ProviderIcon provider="grok" />);
|
||||
const icon = screen.getByTestId("xai-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "var(--text)" });
|
||||
});
|
||||
|
||||
// Opencode provider tests
|
||||
it("renders Opencode brand icon for opencode provider", () => {
|
||||
render(<ProviderIcon provider="opencode" />);
|
||||
expect(screen.getByTestId("opencode-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Opencode")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies provider-specific color for opencode", () => {
|
||||
render(<ProviderIcon provider="opencode" />);
|
||||
const icon = screen.getByTestId("opencode-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-opencode)" });
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for opencode", () => {
|
||||
render(<ProviderIcon provider="opencode" />);
|
||||
const svg = screen.getByTestId("opencode-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-opencode)");
|
||||
});
|
||||
|
||||
it("normalizes Opencode (capitalized) to opencode", () => {
|
||||
render(<ProviderIcon provider="Opencode" />);
|
||||
expect(screen.getByTestId("opencode-icon")).toBeInTheDocument();
|
||||
const wrapper = screen.getByTestId("opencode-icon").parentElement;
|
||||
expect(wrapper).toHaveAttribute("data-provider", "opencode");
|
||||
});
|
||||
|
||||
// Regression test: verify the Kimi icon is the crescent moon shape, not the old "K" placeholder
|
||||
it("renders crescent moon icon geometry (not the old K placeholder)", () => {
|
||||
render(<ProviderIcon provider="kimi" />);
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
--provider-minimax: #e73562;
|
||||
--provider-zai: #1a6dff;
|
||||
--provider-kimi: #6c5ce7;
|
||||
--provider-opencode: #38bdf8;
|
||||
--provider-icon-contrast: var(--bg);
|
||||
|
||||
/* Task-creation CTA tokens */
|
||||
|
||||
Reference in New Issue
Block a user