FN-8500: add Xiaomi provider icons

Show Xiaomi branding for direct provider names and MiMo model labels.

- Add an accessible, tokenized Xiaomi provider mark
- Infer boundary-safe MiMo labels through the shared provider icon key
- Reuse shared inference in usage cards and cover Xiaomi mappings
- Add a patch changeset for Xiaomi provider branding

Files changed:
 .changeset/fn-8500-xiaomi-provider-icon.md         |  7 +++
 packages/dashboard/app/components/ProviderIcon.tsx | 24 +++++++++
 .../dashboard/app/components/UsageIndicator.tsx    | 58 +++-------------------
 .../app/components/__tests__/ProviderIcon.test.tsx | 48 ++++++++++++++++++
 .../components/__tests__/UsageIndicator.test.tsx   | 17 +++++++
 packages/dashboard/app/styles.css                  |  1 +
 .../app/utils/__tests__/providerIconKey.test.ts    |  9 ++++
 packages/dashboard/app/utils/providerIconKey.ts    |  8 +++
 8 files changed, 120 insertions(+), 52 deletions(-)

Fusion-Task-Id: FN-8500

Fusion-Task-Lineage: 1784a180-cf41-4666-a59e-10aa24f47b72

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-22 17:29:20 -07:00
parent 507ae2487f
commit c469f90b12
8 changed files with 120 additions and 52 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Show Xiaomi branding for Xiaomi and MiMo provider labels.
category: feature
dev: Adds a tokenized shared ProviderIcon mark and boundary-safe MiMo inference.

View File

@@ -258,6 +258,29 @@ function OpencodeIcon({ size, color, label = "Opencode" }: { size: number; color
); );
} }
/*
FNXC:ProviderIcons 2026-07-22-17:04:
FN-8500 requires Hermes-supported Xiaomi and confirmed MiMo labels to share one accessible brand mark across dashboard surfaces. This inline path is Xiaomi's Simple Icons mark (CC0 1.0); keep its color supplied by the provider token rather than hardcoding presentation in TSX.
*/
function XiaomiIcon({ size, color, label = "Xiaomi" }: { 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="xiaomi-icon"
aria-label={label}
>
<path
d="M12 0C8.016 0 4.756.255 2.493 2.516.23 4.776 0 8.033 0 12.012c0 3.98.23 7.235 2.494 9.497C4.757 23.77 8.017 24 12 24c3.983 0 7.243-.23 9.506-2.491C23.77 19.247 24 15.99 24 12.012c0-3.984-.233-7.243-2.502-9.504C19.234.252 15.978 0 12 0zM4.906 7.405h5.624c1.47 0 3.007.068 3.764.827.746.746.827 2.233.83 3.676v4.54a.15.15 0 0 1-.152.147h-1.947a.15.15 0 0 1-.152-.148V11.83c-.002-.806-.048-1.634-.464-2.051-.358-.36-1.026-.441-1.72-.458H7.158a.15.15 0 0 0-.151.147v6.98a.15.15 0 0 1-.152.148H4.906a.15.15 0 0 1-.15-.148V7.554a.15.15 0 0 1 .15-.149zm12.131 0h1.949a.15.15 0 0 1 .15.15v8.892a.15.15 0 0 1-.15.148h-1.949a.15.15 0 0 1-.151-.148V7.554a.15.15 0 0 1 .151-.149zM8.92 10.948h2.046c.083 0 .15.066.15.147v5.352a.15.15 0 0 1-.15.148H8.92a.15.15 0 0 1-.152-.148v-5.352a.15.15 0 0 1 .152-.147Z"
fill={color}
/>
</svg>
);
}
function DeepSeekIcon({ size, color, label = "DeepSeek" }: { size: number; color: string; label?: string }) { function DeepSeekIcon({ size, color, label = "DeepSeek" }: { size: number; color: string; label?: string }) {
return ( return (
<svg <svg
@@ -841,6 +864,7 @@ const providerConfig: Record<
minimax: { component: MiniMaxIcon, color: "var(--provider-minimax)" }, minimax: { component: MiniMaxIcon, color: "var(--provider-minimax)" },
"minimax-cn": { component: MiniMaxIcon, color: "var(--provider-minimax)", label: "MiniMax (CN)" }, "minimax-cn": { component: MiniMaxIcon, color: "var(--provider-minimax)", label: "MiniMax (CN)" },
zai: { component: ZaiIcon, color: "var(--provider-zai)" }, zai: { component: ZaiIcon, color: "var(--provider-zai)" },
xiaomi: { component: XiaomiIcon, color: "var(--provider-xiaomi)" },
kimi: { component: KimiIcon, color: "var(--provider-kimi)" }, kimi: { component: KimiIcon, color: "var(--provider-kimi)" },
moonshot: { component: KimiIcon, color: "var(--provider-kimi)" }, // Moonshot alias moonshot: { component: KimiIcon, color: "var(--provider-kimi)" }, // Moonshot alias

View File

@@ -5,6 +5,7 @@ import { X, RefreshCw, Activity, TrendingUp, CheckCircle, AlertTriangle, Eye, Gr
import type { ProviderUsage, UsageWindow } from "../api"; import type { ProviderUsage, UsageWindow } from "../api";
import { useUsageData } from "../hooks/useUsageData"; import { useUsageData } from "../hooks/useUsageData";
import { ProviderIcon } from "./ProviderIcon"; import { ProviderIcon } from "./ProviderIcon";
import { inferProviderIconKey } from "../utils/providerIconKey";
import { getScopedItem, setScopedItem } from "../utils/projectStorage"; import { getScopedItem, setScopedItem } from "../utils/projectStorage";
import "./UsageIndicator.css"; import "./UsageIndicator.css";
@@ -408,57 +409,10 @@ interface ProviderCardProps {
onMoveDown: () => void; onMoveDown: () => void;
} }
/** /*
* Map provider names to ProviderIcon provider keys FNXC:UsageIndicator 2026-07-22-17:04:
*/ FN-8500 makes usage cards consume the canonical provider/model inference seam rather than duplicate it. This keeps Xiaomi MiMo labels and every existing shared mapping aligned with model, task, and analytics surfaces.
function getProviderIconKey(providerName: string): string { */
const normalized = providerName.toLowerCase();
// Map common provider names to their icon keys
if (normalized.includes('claude') || normalized.includes('anthropic')) {
return 'anthropic';
}
if (normalized.includes('codex') || normalized.includes('openai') || normalized.includes('gpt')) {
return 'openai';
}
if (normalized.includes('gemini') || normalized.includes('google') || normalized.includes('antigravity')) {
return 'google';
}
if (normalized.includes('ollama')) {
return 'ollama';
}
if (normalized.includes('minimax')) {
return 'minimax';
}
if (normalized.includes('zai') || normalized.includes('zhipu')) {
return 'zai';
}
if (normalized.includes('kimi') || normalized.includes('moonshot')) {
return 'kimi';
}
if (normalized.includes('bedrock') || normalized.includes('amazon')) {
return 'bedrock';
}
if (normalized.includes('xai') || normalized.includes('grok')) {
return 'xai';
}
/*
FNXC:UsageIndicator 2026-07-10-00:00:
Cursor usage cards are rendered by the generic ProviderCard path, so provider-name mapping is the only frontend-specific requirement: route "Cursor" to the existing cursor-cli icon token and SVG.
*/
if (normalized.includes('cursor')) {
return 'cursor-cli';
}
if (normalized.includes('opencode')) {
return 'opencode';
}
if (normalized.includes('copilot') || normalized === 'github copilot') {
return 'github-copilot';
}
// Return the original name as fallback (ProviderIcon will show a default icon)
return providerName;
}
/** /**
* Provider card showing status and usage windows * Provider card showing status and usage windows
@@ -522,7 +476,7 @@ function ProviderCard({
> >
<div className="usage-provider-header"> <div className="usage-provider-header">
<div className="usage-provider-info"> <div className="usage-provider-info">
<ProviderIcon provider={getProviderIconKey(provider.name)} size="md" /> <ProviderIcon provider={inferProviderIconKey(provider.name)} size="md" />
<span className="usage-provider-name">{provider.name}</span> <span className="usage-provider-name">{provider.name}</span>
{hiddenCount > 0 && ( {hiddenCount > 0 && (
<button <button

View File

@@ -28,6 +28,7 @@ const SYNTHETIC_FIRST_CLASS_PROVIDER_IDS = [
"paperclip-ai", "paperclip-ai",
"omp", "omp",
"oh-my-pi", "oh-my-pi",
"xiaomi",
] as const; ] as const;
const FIRST_CLASS_PROVIDER_IDS = [...new Set([ const FIRST_CLASS_PROVIDER_IDS = [...new Set([
@@ -423,6 +424,37 @@ describe("ProviderIcon", () => {
expect(wrapper).toHaveAttribute("data-provider", "minimax"); expect(wrapper).toHaveAttribute("data-provider", "minimax");
}); });
it.each([
["xiaomi", "xiaomi"],
["XIAOMI", "xiaomi"],
])("renders an accessible tokenized Xiaomi mark for %s", (provider, expectedDataProvider) => {
render(<ProviderIcon provider={provider} size="md" />);
const svg = screen.getByTestId("xiaomi-icon");
const wrapper = svg.parentElement;
expect(svg).toHaveAccessibleName("Xiaomi");
expect(isNonCpuMark(svg)).toBe(true);
expect(wrapper).toHaveAttribute("data-provider", expectedDataProvider);
expect(wrapper).toHaveStyle({ color: "var(--provider-xiaomi)" });
expect(svg.querySelector("path")).toHaveAttribute("fill", "var(--provider-xiaomi)");
expect(svg).toHaveAttribute("width", "20");
expect(svg).toHaveAttribute("height", "20");
});
it("renders independent Xiaomi SVGs for duplicate provider entries", () => {
const { container } = render(
<>
<ProviderIcon provider="xiaomi" size="sm" />
<ProviderIcon provider="xiaomi" size="lg" />
</>,
);
const icons = container.querySelectorAll("[data-testid='xiaomi-icon']");
expect(icons).toHaveLength(2);
expect(Array.from(icons).map((icon) => icon.getAttribute("width"))).toEqual(["16", "24"]);
expect(Array.from(icons).every((icon) => isNonCpuMark(icon as SVGElement))).toBe(true);
});
it("renders Z.ai brand icon for zai provider", () => { it("renders Z.ai brand icon for zai provider", () => {
render(<ProviderIcon provider="zai" />); render(<ProviderIcon provider="zai" />);
expect(screen.getByTestId("zai-icon")).toBeInTheDocument(); expect(screen.getByTestId("zai-icon")).toBeInTheDocument();
@@ -561,6 +593,22 @@ describe("ProviderIcon", () => {
expect(icon).toHaveAttribute("data-provider", expectedDataProvider); expect(icon).toHaveAttribute("data-provider", expectedDataProvider);
}); });
it.each(["xiaomi/MiMo-V2-Flash", "MiMo-V2-Flash"])('infers Xiaomi icon from the boundary-safe MiMo model label %s', (provider) => {
render(<ProviderIcon provider={provider} />);
const svg = screen.getByTestId("xiaomi-icon");
expect(svg).toHaveAccessibleName("Xiaomi");
expect(isNonCpuMark(svg)).toBe(true);
});
it("preserves the CPU fallback for Xiaomi near-match custom names", () => {
const { container } = render(<ProviderIcon provider="mimosa-v1" />);
const svg = container.querySelector<SVGElement>('[data-provider="mimosa-v1"] svg');
expect(svg).toBeTruthy();
expect(hasLucideCpuSignature(svg as SVGElement)).toBe(true);
});
it("infers non-Grok provider icons from model-shaped ids", () => { it("infers non-Grok provider icons from model-shaped ids", () => {
render(<ProviderIcon provider="gpt-5.5" />); render(<ProviderIcon provider="gpt-5.5" />);
const icon = screen.getByTestId("openai-icon").parentElement; const icon = screen.getByTestId("openai-icon").parentElement;

View File

@@ -1536,6 +1536,23 @@ describe("UsageIndicator", () => {
expect(screen.getByText("Moonshot")).toBeInTheDocument(); expect(screen.getByText("Moonshot")).toBeInTheDocument();
}); });
it("maps Xiaomi MiMo usage labels through the shared provider inference", () => {
mockUseUsageData.mockReturnValue(createUsageDataState({
providers: [
{ name: "xiaomi/MiMo-V2-Flash", icon: "🟠", status: "ok", windows: [] },
],
loading: false,
error: null,
lastUpdated: new Date(),
refresh: mockRefresh,
}));
render(<UsageIndicator isOpen={true} onClose={mockOnClose} projectId={TEST_PROJECT_ID} />);
expect(screen.getByTestId("xiaomi-icon")).toBeInTheDocument();
expect(document.querySelector('[data-provider="xiaomi"]')).toBeInTheDocument();
});
it("maps Cursor provider to the cursor-cli icon and renders usage windows", () => { it("maps Cursor provider to the cursor-cli icon and renders usage windows", () => {
mockUseUsageData.mockReturnValue(createUsageDataState({ mockUseUsageData.mockReturnValue(createUsageDataState({
providers: [ providers: [

View File

@@ -380,6 +380,7 @@ svg.spinning {
--provider-tavily: #4f46e5; --provider-tavily: #4f46e5;
--provider-minimax: #e73562; --provider-minimax: #e73562;
--provider-zai: #1a6dff; --provider-zai: #1a6dff;
--provider-xiaomi: #ff6900;
--provider-kimi: #6c5ce7; --provider-kimi: #6c5ce7;
--provider-bedrock: #ff9900; --provider-bedrock: #ff9900;
--provider-opencode: #38bdf8; --provider-opencode: #38bdf8;

View File

@@ -16,6 +16,10 @@ describe("inferProviderIconKey", () => {
["omp-cli/MiniMax-M2.5", "omp-cli"], ["omp-cli/MiniMax-M2.5", "omp-cli"],
["omp/claude-sonnet-4", "omp-cli"], ["omp/claude-sonnet-4", "omp-cli"],
["Oh My Pi", "omp-cli"], ["Oh My Pi", "omp-cli"],
["xiaomi", "xiaomi"],
["Xiaomi", "xiaomi"],
["xiaomi/MiMo-V2-Flash", "xiaomi"],
["MiMo-V2-Flash", "xiaomi"],
["ollama/llama3", "ollama"], ["ollama/llama3", "ollama"],
["minimax-text-01", "minimax"], ["minimax-text-01", "minimax"],
["zhipu-glm-4", "zai"], ["zhipu-glm-4", "zai"],
@@ -40,6 +44,11 @@ describe("inferProviderIconKey", () => {
expect(inferProviderIconKey("claude-sonnet-4")).not.toBe("omp-cli"); expect(inferProviderIconKey("claude-sonnet-4")).not.toBe("omp-cli");
}); });
it("does not infer Xiaomi from near-match custom names", () => {
expect(inferProviderIconKey("mimosa-v1")).toBe("mimosa-v1");
expect(inferProviderIconKey("custom-mimoized-model")).toBe("custom-mimoized-model");
});
it("returns unknown ids unchanged so ProviderIcon can render its fallback", () => { it("returns unknown ids unchanged so ProviderIcon can render its fallback", () => {
expect(inferProviderIconKey("custom-model-v1")).toBe("custom-model-v1"); expect(inferProviderIconKey("custom-model-v1")).toBe("custom-model-v1");
expect(inferProviderIconKey("not-a-glmish-model")).toBe("not-a-glmish-model"); expect(inferProviderIconKey("not-a-glmish-model")).toBe("not-a-glmish-model");

View File

@@ -8,6 +8,7 @@ Standalone GLM model ids such as glm-5.1, glm-4.5-air, and glm-5v-turbo are Z.ai
const GLM_MODEL_SEGMENT_PATTERN = /(?:^|[/:_-])glm(?:$|[\d._:-]|-[\da-z])/; const GLM_MODEL_SEGMENT_PATTERN = /(?:^|[/:_-])glm(?:$|[\d._:-]|-[\da-z])/;
const OMP_PROVIDER_SEGMENT_PATTERN = /(?:^|[/:_\s-])omp(?:$|[/:_\s-])/; const OMP_PROVIDER_SEGMENT_PATTERN = /(?:^|[/:_\s-])omp(?:$|[/:_\s-])/;
const OMP_PROVIDER_NAME_PATTERN = /oh[-\s]my[-\s]pi/; const OMP_PROVIDER_NAME_PATTERN = /oh[-\s]my[-\s]pi/;
const XIAOMI_MIMO_MODEL_SEGMENT_PATTERN = /(?:^|[/:_-])mimo(?:$|[/:_.-])/;
export function inferProviderIconKey(modelOrProviderName: string): string { export function inferProviderIconKey(modelOrProviderName: string): string {
const normalized = modelOrProviderName.toLowerCase(); const normalized = modelOrProviderName.toLowerCase();
@@ -28,6 +29,13 @@ export function inferProviderIconKey(modelOrProviderName: string): string {
if (OMP_PROVIDER_SEGMENT_PATTERN.test(normalized) || OMP_PROVIDER_NAME_PATTERN.test(normalized)) { if (OMP_PROVIDER_SEGMENT_PATTERN.test(normalized) || OMP_PROVIDER_NAME_PATTERN.test(normalized)) {
return "omp-cli"; return "omp-cli";
} }
/*
FNXC:ProviderIcons 2026-07-22-17:04:
FN-8500 requires the Hermes Xiaomi provider and Xiaomi MiMo model IDs to resolve through the shared icon path. Restrict MiMo matching to model-id segment boundaries so unrelated custom names such as "mimosa" retain the fallback.
*/
if (normalized === "xiaomi" || XIAOMI_MIMO_MODEL_SEGMENT_PATTERN.test(normalized)) {
return "xiaomi";
}
if (normalized.includes("claude") || normalized.includes("anthropic")) { if (normalized.includes("claude") || normalized.includes("anthropic")) {
return "anthropic"; return "anthropic";
} }