From 7c7b6555f17a88cffddf484129cd04ad79704f59 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:00:36 +0200 Subject: [PATCH] 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 Co-authored-by: Lu * 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 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Lu --- .../browser/provider-catalog-icons.spec.ts | 51 +++++++++++++++++++ packages/app/src/assets/acp-provider-icons.ts | 9 +++- .../src/assets/acp-provider-icons/devin.svg | 3 ++ .../src/assets/acp-provider-icons/grok.svg | 4 ++ .../src/assets/acp-provider-icons/hermes.svg | 3 ++ .../src/assets/acp-provider-icons/kiro.svg | 1 + .../acp-provider-icons/minimax-code.svg | 3 ++ .../src/assets/acp-provider-icons/traecli.svg | 3 ++ .../src/components/icons/antigravity-icon.tsx | 17 +++++++ .../app/src/components/icons/kiro-icon.tsx | 18 +++++++ .../src/components/provider-icon-name.test.ts | 2 + .../app/src/components/provider-icons.test.ts | 12 ++++- packages/app/src/components/provider-icons.ts | 7 ++- packages/app/src/data/acp-provider-catalog.ts | 6 +-- .../hooks/use-acp-provider-catalog.test.ts | 7 +++ packages/protocol/src/provider-icon-names.ts | 3 ++ packages/website/e2e/agent-icons.spec.ts | 26 ++++++++++ .../website/src/assets/agent-icons/hermes.svg | 3 ++ 18 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 packages/app/e2e/browser/provider-catalog-icons.spec.ts create mode 100644 packages/app/src/assets/acp-provider-icons/devin.svg create mode 100644 packages/app/src/assets/acp-provider-icons/grok.svg create mode 100644 packages/app/src/assets/acp-provider-icons/hermes.svg create mode 100644 packages/app/src/assets/acp-provider-icons/kiro.svg create mode 100644 packages/app/src/assets/acp-provider-icons/minimax-code.svg create mode 100644 packages/app/src/assets/acp-provider-icons/traecli.svg create mode 100644 packages/app/src/components/icons/antigravity-icon.tsx create mode 100644 packages/app/src/components/icons/kiro-icon.tsx create mode 100644 packages/website/e2e/agent-icons.spec.ts create mode 100644 packages/website/src/assets/agent-icons/hermes.svg diff --git a/packages/app/e2e/browser/provider-catalog-icons.spec.ts b/packages/app/e2e/browser/provider-catalog-icons.spec.ts new file mode 100644 index 0000000000..818d88ab84 --- /dev/null +++ b/packages/app/e2e/browser/provider-catalog-icons.spec.ts @@ -0,0 +1,51 @@ +import { ACP_PROVIDER_ICON_SVGS } from "../../src/assets/acp-provider-icons"; +import { expect, test, type Page } from "../support/fixtures"; +import type { TestInfo } from "@playwright/test"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { getServerId } from "../support/helpers/server-id"; +import { + findAcpCatalogProvider, + openAddProviderArea, + openSettingsHost, + openSettingsHostSection, +} from "../support/helpers/settings"; + +async function openProviderCatalog(page: Page) { + await gotoAppShell(page); + await openSettings(page); + await openSettingsHost(page, getServerId()); + await openSettingsHostSection(page, getServerId(), "providers"); + await openAddProviderArea(page); +} + +async function expectProviderArtwork( + page: Page, + testInfo: TestInfo, + id: "devin" | "hermes" | "kiro", + name: string, +) { + await findAcpCatalogProvider(page, name); + // Catalog tiles have no semantic role; scope artwork to the named provider's tile. + const icon = page.getByTestId(`provider-catalog-tile-${id}`).locator("svg").first(); + await expect(icon).toBeVisible(); + // Provider artwork is 24px; the generic PackagePlus fallback is 20px. + await expect.soft(icon).toHaveAttribute("width", "24"); + const expectedPaths = await page.evaluate((xml) => { + const document = new DOMParser().parseFromString(xml, "image/svg+xml"); + return Array.from(document.querySelectorAll("path"), (path) => path.getAttribute("d")!); + }, ACP_PROVIDER_ICON_SVGS[id]); + await expect(icon.locator("path")).toHaveCount(expectedPaths.length); + for (const [index, path] of expectedPaths.entries()) { + await expect(icon.locator("path").nth(index)).toHaveAttribute("d", path); + } + await page.screenshot({ path: testInfo.outputPath(`${id}-catalog.png`) }); +} + +test("#7087: the provider catalog shows Devin, Hermes, and Kiro artwork", async ({ + page, +}, testInfo) => { + await openProviderCatalog(page); + await expectProviderArtwork(page, testInfo, "devin", "Devin CLI"); + await expectProviderArtwork(page, testInfo, "hermes", "Hermes"); + await expectProviderArtwork(page, testInfo, "kiro", "Kiro CLI"); +}); diff --git a/packages/app/src/assets/acp-provider-icons.ts b/packages/app/src/assets/acp-provider-icons.ts index cebb4ca76d..703473e6be 100644 --- a/packages/app/src/assets/acp-provider-icons.ts +++ b/packages/app/src/assets/acp-provider-icons.ts @@ -53,9 +53,10 @@ export const ACP_PROVIDER_ICON_SVGS = { junie: '\n \n \n \n\n', kilo: '\n \n\n', + kiro: ' ', kimi: '\n \n\n', "minimax-code": - '\n \n\n', + '\n \n\n', "minion-code": '\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n', "mistral-vibe": @@ -74,7 +75,11 @@ export const ACP_PROVIDER_ICON_SVGS = { stakpak: '\n \n\n', traecli: - '\n \n\n', + '\n \n\n', vtcode: '\n \n VT\n\n', + devin: + '\n \n\n', + hermes: + '\n \n\n', } as const; diff --git a/packages/app/src/assets/acp-provider-icons/devin.svg b/packages/app/src/assets/acp-provider-icons/devin.svg new file mode 100644 index 0000000000..e0f58c6513 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/devin.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/grok.svg b/packages/app/src/assets/acp-provider-icons/grok.svg new file mode 100644 index 0000000000..35db840319 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/grok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/app/src/assets/acp-provider-icons/hermes.svg b/packages/app/src/assets/acp-provider-icons/hermes.svg new file mode 100644 index 0000000000..c6fdd22500 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/hermes.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/kiro.svg b/packages/app/src/assets/acp-provider-icons/kiro.svg new file mode 100644 index 0000000000..31715c335a --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/kiro.svg @@ -0,0 +1 @@ + diff --git a/packages/app/src/assets/acp-provider-icons/minimax-code.svg b/packages/app/src/assets/acp-provider-icons/minimax-code.svg new file mode 100644 index 0000000000..61fcb8afab --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/minimax-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/traecli.svg b/packages/app/src/assets/acp-provider-icons/traecli.svg new file mode 100644 index 0000000000..6d5388fcaa --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/traecli.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/components/icons/antigravity-icon.tsx b/packages/app/src/components/icons/antigravity-icon.tsx new file mode 100644 index 0000000000..baf2b4c4a6 --- /dev/null +++ b/packages/app/src/components/icons/antigravity-icon.tsx @@ -0,0 +1,17 @@ +import Svg, { Path } from "react-native-svg"; + +interface AntigravityIconProps { + size?: number; + color?: string; +} + +export function AntigravityIcon({ size = 16, color = "currentColor" }: AntigravityIconProps) { + return ( + + + + ); +} diff --git a/packages/app/src/components/icons/kiro-icon.tsx b/packages/app/src/components/icons/kiro-icon.tsx new file mode 100644 index 0000000000..830e9109aa --- /dev/null +++ b/packages/app/src/components/icons/kiro-icon.tsx @@ -0,0 +1,18 @@ +import Svg, { Path } from "react-native-svg"; + +interface KiroIconProps { + size?: number; + color?: string; +} + +export function KiroIcon({ size = 16, color = "currentColor" }: KiroIconProps) { + return ( + + + + ); +} diff --git a/packages/app/src/components/provider-icon-name.test.ts b/packages/app/src/components/provider-icon-name.test.ts index 65c75166a5..0083926627 100644 --- a/packages/app/src/components/provider-icon-name.test.ts +++ b/packages/app/src/components/provider-icon-name.test.ts @@ -17,8 +17,10 @@ describe("resolveProviderIconName", () => { it("returns the catalog identifier for ACP catalog provider ids that ship an icon", () => { expect(resolveProviderIconName("amp-acp")).toEqual({ kind: "catalog", id: "amp-acp" }); + expect(resolveProviderIconName("devin")).toEqual({ kind: "catalog", id: "devin" }); expect(resolveProviderIconName("gemini")).toEqual({ kind: "catalog", id: "gemini" }); expect(resolveProviderIconName("gjc")).toEqual({ kind: "catalog", id: "gjc" }); + expect(resolveProviderIconName("hermes")).toEqual({ kind: "catalog", id: "hermes" }); expect(resolveProviderIconName("traecli")).toEqual({ kind: "catalog", id: "traecli" }); }); diff --git a/packages/app/src/components/provider-icons.test.ts b/packages/app/src/components/provider-icons.test.ts index 025ff14bef..aceed473be 100644 --- a/packages/app/src/components/provider-icons.test.ts +++ b/packages/app/src/components/provider-icons.test.ts @@ -1,6 +1,6 @@ // @vitest-environment jsdom import { act, cleanup, renderHook } from "@testing-library/react"; -import { Bot } from "lucide-react-native"; +import { Bot, PackagePlus } from "lucide-react-native"; import { SvgXml } from "react-native-svg"; import { afterEach, describe, expect, it } from "vitest"; import { replaceProviderSnapshotIcons } from "@/data/provider-icons"; @@ -17,6 +17,16 @@ function renderIcon(Component: ProviderIconComponent) { } describe("useProviderIcon", () => { + it.each(["devin", "hermes", "kiro", "antigravity"])( + "#7087: %s has a provider mark before a host snapshot arrives", + (provider) => { + const Icon = renderHook(() => useProviderIcon(provider)).result.current; + expect(Icon).not.toBe(Bot); + expect(Icon).not.toBe(PackagePlus); + expect(typeof Icon).toBe("function"); + }, + ); + it("renders registered snapshot SVG metadata with the requested size and color", () => { const svg = ''; replaceProviderSnapshotIcons("server-1", [{ provider: "rendered-provider", iconSvg: svg }]); diff --git a/packages/app/src/components/provider-icons.ts b/packages/app/src/components/provider-icons.ts index 883b4820f7..d8548cbd16 100644 --- a/packages/app/src/components/provider-icons.ts +++ b/packages/app/src/components/provider-icons.ts @@ -1,12 +1,14 @@ import { useStore } from "zustand"; import { providerSnapshotIcons } from "@/data/provider-icons"; -import { Bot, PackagePlus } from "lucide-react-native"; +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"; @@ -23,10 +25,11 @@ export interface ProviderIconProps { export type ProviderIconComponent = ComponentType; const BUILTIN_PROVIDER_ICONS: Record = { + 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: PackagePlus, + kiro: KiroIcon as unknown as ProviderIconComponent, minimax: MiniMaxIcon as unknown as ProviderIconComponent, omp: OmpIcon as unknown as ProviderIconComponent, opencode: OpenCodeIcon as unknown as ProviderIconComponent, diff --git a/packages/app/src/data/acp-provider-catalog.ts b/packages/app/src/data/acp-provider-catalog.ts index 1bd32db53d..f91a9540f2 100644 --- a/packages/app/src/data/acp-provider-catalog.ts +++ b/packages/app/src/data/acp-provider-catalog.ts @@ -133,7 +133,7 @@ const CATALOG_DATA = [ title: "Devin CLI", description: "Cognition's Devin for Terminal via Agent Client Protocol", version: "manual", - iconId: null, + iconId: "devin", installLink: "https://cli.devin.ai/docs", command: ["devin", "acp"], }, @@ -235,7 +235,7 @@ const CATALOG_DATA = [ title: "Hermes", description: "Nous Research self-improving AI agent", version: "manual", - iconId: null, + iconId: "hermes", installLink: "https://hermes-agent.nousresearch.com/docs/user-guide/features/acp", command: ["hermes", "acp"], }, @@ -272,7 +272,7 @@ const CATALOG_DATA = [ title: "Kiro CLI", description: "Amazon's AI coding agent with native ACP support", version: "manual", - iconId: null, + iconId: "kiro", installLink: "https://kiro.dev/docs/cli/acp/", command: ["kiro-cli", "acp"], }, diff --git a/packages/app/src/hooks/use-acp-provider-catalog.test.ts b/packages/app/src/hooks/use-acp-provider-catalog.test.ts index 201c17c022..c5fd59d53e 100644 --- a/packages/app/src/hooks/use-acp-provider-catalog.test.ts +++ b/packages/app/src/hooks/use-acp-provider-catalog.test.ts @@ -11,6 +11,13 @@ function findProvider(id: string) { } describe("ACP provider catalog", () => { + it.each(["devin", "hermes", "kiro"])( + "#7087: %s supplies its provider mark to the catalog picker", + (provider) => { + expect(findProvider(provider).iconSvg).toContain(" { const ids = new Set(); diff --git a/packages/protocol/src/provider-icon-names.ts b/packages/protocol/src/provider-icon-names.ts index 4ca8ea44e3..5f707dfeff 100644 --- a/packages/protocol/src/provider-icon-names.ts +++ b/packages/protocol/src/provider-icon-names.ts @@ -1,4 +1,5 @@ export const BUILTIN_PROVIDER_ICON_NAMES = [ + "antigravity", "claude", "codex", "copilot", @@ -22,6 +23,7 @@ export const ACP_PROVIDER_ICON_NAMES = [ "crow-cli", "cursor", "deepagents", + "devin", "dimcode", "dirac", "factory-droid", @@ -31,6 +33,7 @@ export const ACP_PROVIDER_ICON_NAMES = [ "glm-acp-agent", "goose", "grok", + "hermes", "hoplite", "junie", "kilo", diff --git a/packages/website/e2e/agent-icons.spec.ts b/packages/website/e2e/agent-icons.spec.ts new file mode 100644 index 0000000000..4bc828475e --- /dev/null +++ b/packages/website/e2e/agent-icons.spec.ts @@ -0,0 +1,26 @@ +import { expect, test, type Page } from "playwright/test"; + +test("keeps unrelated agent artwork unchanged while adding the Hermes mark", async ({ page }) => { + await openSupportedAgents(page); + await expectGenericAgentMark(page, "CodeWhale"); + await expectVendoredAgentMark(page, "Hermes Agent"); +}); + +async function openSupportedAgents(page: Page) { + await page.goto("/agents"); + await expect(page.getByRole("heading", { level: 1, name: "Supported agents" })).toBeVisible(); +} + +async function expectGenericAgentMark(page: Page, name: string) { + const card = page.getByRole("link", { name, exact: true }); + await expect(card).toBeVisible(); + // Decorative SVGs have no accessible role; the fallback is Lucide's bot mark. + await expect(card.locator("svg.lucide-bot")).toBeVisible(); +} + +async function expectVendoredAgentMark(page: Page, name: string) { + const card = page.getByRole("link", { name, exact: true }); + await expect(card).toBeVisible(); + await expect(card.locator("svg")).toBeVisible(); + await expect(card.locator("svg.lucide-bot")).toHaveCount(0); +} diff --git a/packages/website/src/assets/agent-icons/hermes.svg b/packages/website/src/assets/agent-icons/hermes.svg new file mode 100644 index 0000000000..c6fdd22500 --- /dev/null +++ b/packages/website/src/assets/agent-icons/hermes.svg @@ -0,0 +1,3 @@ + + +