From 352e8fb6eb1e1226912d1f950cb70c3f27f41464 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 19 May 2026 20:54:33 +0800 Subject: [PATCH] Show catalog provider icons (#1098) --- .../app/src/components/provider-icons.test.ts | 30 ++++++++++ packages/app/src/components/provider-icons.ts | 60 ++++++++++++++++--- 2 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 packages/app/src/components/provider-icons.test.ts diff --git a/packages/app/src/components/provider-icons.test.ts b/packages/app/src/components/provider-icons.test.ts new file mode 100644 index 000000000..681f44ecf --- /dev/null +++ b/packages/app/src/components/provider-icons.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("lucide-react-native", () => ({ + Bot: function Bot() { + return null; + }, + PackagePlus: function PackagePlus() { + return null; + }, +})); + +import { Bot, PackagePlus } from "lucide-react-native"; +import { getProviderIcon } from "./provider-icons"; + +describe("getProviderIcon", () => { + it("keeps built-in provider icons", () => { + expect(getProviderIcon("kiro")).toBe(PackagePlus); + }); + + it("uses vendored ACP catalog icons for catalog provider ids", () => { + const icon = getProviderIcon("amp-acp"); + + expect(icon).not.toBe(Bot); + expect(getProviderIcon("amp-acp")).toBe(icon); + }); + + it("falls back to the robot icon for unknown custom providers", () => { + expect(getProviderIcon("custom-claude-profile")).toBe(Bot); + }); +}); diff --git a/packages/app/src/components/provider-icons.ts b/packages/app/src/components/provider-icons.ts index 1a85cf97f..941c861ad 100644 --- a/packages/app/src/components/provider-icons.ts +++ b/packages/app/src/components/provider-icons.ts @@ -1,19 +1,63 @@ import { Bot, PackagePlus } from "lucide-react-native"; +import { createElement, type ComponentType } from "react"; +import { SvgXml } from "react-native-svg"; import { ClaudeIcon } from "@/components/icons/claude-icon"; import { CodexIcon } from "@/components/icons/codex-icon"; import { CopilotIcon } from "@/components/icons/copilot-icon"; import { OpenCodeIcon } from "@/components/icons/opencode-icon"; import { PiIcon } from "@/components/icons/pi-icon"; +import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog"; -const PROVIDER_ICONS: Record = { - claude: ClaudeIcon as unknown as typeof Bot, - codex: CodexIcon as unknown as typeof Bot, - copilot: CopilotIcon as unknown as typeof Bot, +export interface ProviderIconProps { + size: number; + color: string; +} + +export type ProviderIconComponent = ComponentType; + +const PROVIDER_ICONS: Record = { + claude: ClaudeIcon as unknown as ProviderIconComponent, + codex: CodexIcon as unknown as ProviderIconComponent, + copilot: CopilotIcon as unknown as ProviderIconComponent, kiro: PackagePlus, - opencode: OpenCodeIcon as unknown as typeof Bot, - pi: PiIcon as unknown as typeof Bot, + opencode: OpenCodeIcon as unknown as ProviderIconComponent, + pi: PiIcon as unknown as ProviderIconComponent, }; -export function getProviderIcon(provider: string): typeof Bot { - return PROVIDER_ICONS[provider] ?? Bot; +const CATALOG_ICON_SVGS = new Map( + ACP_PROVIDER_CATALOG.flatMap((entry) => (entry.iconSvg ? [[entry.id, entry.iconSvg]] : [])), +); + +const catalogIconComponents = new Map(); + +function createCatalogIcon(provider: string, iconSvg: string): ProviderIconComponent { + const CatalogProviderIcon: ProviderIconComponent = ({ size, color }) => + createElement(SvgXml, { + xml: iconSvg, + width: size, + height: size, + color, + }); + CatalogProviderIcon.displayName = `CatalogProviderIcon(${provider})`; + return CatalogProviderIcon; +} + +function getCatalogProviderIcon(provider: string): ProviderIconComponent | undefined { + const cached = catalogIconComponents.get(provider); + if (cached) { + return cached; + } + + const iconSvg = CATALOG_ICON_SVGS.get(provider); + if (!iconSvg) { + return undefined; + } + + const icon = createCatalogIcon(provider, iconSvg); + catalogIconComponents.set(provider, icon); + return icon; +} + +export function getProviderIcon(provider: string): ProviderIconComponent { + return PROVIDER_ICONS[provider] ?? getCatalogProviderIcon(provider) ?? Bot; }