Files
paseo/packages/app/src/components/provider-icons.ts
T
7c7b6555f1 Show provider artwork in the catalog and provider rows (#7330)
* Add missing monochrome provider icons (kiro, devin, hermes, minimax-code, traecli, grok, deepseek-tui)

* fix(app): add Kiro catalog icon and Antigravity builtin icon

The ACP catalog's kiro entry had iconId null and no vendored svg, and
the native Antigravity provider resolved to the Bot fallback before the
daemon snapshot arrived. Vendor the Kiro brand mark, wire iconId, and
register a builtin AntigravityIcon matching the plugin's theme-aware
icon.svg.

* fix(app): keep Kiro eyes in monochrome icon

* test(providers): cover catalog artwork and snapshot-free icons

Lock down the missing provider marks through the catalog and the shared icon hook. Exercise the rendered catalog artwork in Chromium alongside coverage for the pre-snapshot fallback.

Co-authored-by: kevenhu001-cyber <keven.hu001@gmail.com>
Co-authored-by: Lu <jack1125516912@outlook.com>

* fix(website): preserve unrelated agent directory artwork

Drop the unrelated website mark introduced with the provider artwork. Cover the existing directory fallback and retained provider mark in a browser regression.

---------

Co-authored-by: kevenhu001-cyber <keven.hu001@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Lu <jack1125516912@outlook.com>
2026-10-18 20:00:36 +02:00

118 lines
4.5 KiB
TypeScript

import { useStore } from "zustand";
import { providerSnapshotIcons } from "@/data/provider-icons";
import { Bot } from "lucide-react-native";
import { createElement, useCallback, type ComponentType } from "react";
import { SvgXml } from "react-native-svg";
import { TERMINAL_PROFILE_ICON_NAMES } from "@getpaseo/protocol/provider-icon-names";
import { AntigravityIcon } from "@/components/icons/antigravity-icon";
import { ClaudeIcon } from "@/components/icons/claude-icon";
import { CodexIcon } from "@/components/icons/codex-icon";
import { CopilotIcon } from "@/components/icons/copilot-icon";
import { KiroIcon } from "@/components/icons/kiro-icon";
import { MiniMaxIcon } from "@/components/icons/minimax-icon";
import { OpenCodeIcon } from "@/components/icons/opencode-icon";
import { OmpIcon } from "@/components/icons/omp-icon";
import { PiIcon } from "@/components/icons/pi-icon";
import { ACP_PROVIDER_ICON_SVGS } from "@/assets/acp-provider-icons";
import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog";
import { resolveProviderIconName } from "@/components/provider-icon-name";
export interface ProviderIconProps {
size: number;
color: string;
}
export type ProviderIconComponent = ComponentType<ProviderIconProps>;
const BUILTIN_PROVIDER_ICONS: Record<string, ProviderIconComponent> = {
antigravity: AntigravityIcon as unknown as ProviderIconComponent,
claude: ClaudeIcon as unknown as ProviderIconComponent,
codex: CodexIcon as unknown as ProviderIconComponent,
copilot: CopilotIcon as unknown as ProviderIconComponent,
kiro: KiroIcon as unknown as ProviderIconComponent,
minimax: MiniMaxIcon as unknown as ProviderIconComponent,
omp: OmpIcon as unknown as ProviderIconComponent,
opencode: OpenCodeIcon as unknown as ProviderIconComponent,
pi: PiIcon as unknown as ProviderIconComponent,
};
const CATALOG_ICON_SVGS = new Map<string, string>(
ACP_PROVIDER_CATALOG.flatMap((entry) => (entry.iconSvg ? [[entry.id, entry.iconSvg]] : [])),
);
for (const name of TERMINAL_PROFILE_ICON_NAMES) {
const iconSvg = (ACP_PROVIDER_ICON_SVGS as Record<string, string | undefined>)[name];
if (iconSvg) {
CATALOG_ICON_SVGS.set(name, iconSvg);
}
}
const catalogIconComponents = new Map<string, ProviderIconComponent>();
const snapshotIconComponents = new Map<string, ProviderIconComponent>();
/** Renders a sanitized SVG string from a host catalog, tinted through `currentColor`. */
export function SvgIcon({ svg, size, color }: ProviderIconProps & { svg: string }) {
return createElement(SvgXml, { xml: svg, width: size, height: size, color });
}
function createSvgIcon(provider: string, iconSvg: string): ProviderIconComponent {
const SvgProviderIcon: ProviderIconComponent = ({ size, color }) =>
SvgIcon({ svg: iconSvg, size, color });
SvgProviderIcon.displayName = `SvgProviderIcon(${provider})`;
return SvgProviderIcon;
}
function getCatalogProviderIcon(provider: string): ProviderIconComponent {
const cached = catalogIconComponents.get(provider);
if (cached) {
return cached;
}
const iconSvg = CATALOG_ICON_SVGS.get(provider);
if (!iconSvg) {
return Bot;
}
const icon = createSvgIcon(provider, iconSvg);
catalogIconComponents.set(provider, icon);
return icon;
}
function getSnapshotProviderIcon(provider: string, svg: string): ProviderIconComponent {
const cached = snapshotIconComponents.get(svg);
if (cached) return cached;
const component = createSvgIcon(provider, svg);
snapshotIconComponents.set(svg, component);
return component;
}
function resolveProviderIcon(provider: string, svg?: string): ProviderIconComponent {
const name = resolveProviderIconName(provider, svg);
if (name.kind === "builtin") {
return BUILTIN_PROVIDER_ICONS[name.id];
}
if (name.kind === "catalog") {
return getCatalogProviderIcon(name.id);
}
if (name.kind === "svg") {
return getSnapshotProviderIcon(provider, name.svg);
}
return Bot;
}
export function useProviderIcon(provider: string, serverId?: string | null): ProviderIconComponent {
const svg = useStore(providerSnapshotIcons, (state) =>
serverId ? state.get(serverId)?.get(provider) : undefined,
);
return resolveProviderIcon(provider, svg);
}
/** Subscribe once for consumers that render a collection of providers. */
export function useProviderIcons(serverId?: string | null) {
const icons = useStore(providerSnapshotIcons, (state) =>
serverId ? state.get(serverId) : undefined,
);
return useCallback(
(provider: string) => resolveProviderIcon(provider, icons?.get(provider)),
[icons],
);
}