diff --git a/packages/app/e2e/browser/terminal-profile-icons.spec.ts b/packages/app/e2e/browser/terminal-profile-icons.spec.ts new file mode 100644 index 0000000000..29d786cfd3 --- /dev/null +++ b/packages/app/e2e/browser/terminal-profile-icons.spec.ts @@ -0,0 +1,71 @@ +import { ACP_PROVIDER_ICON_SVGS } from "../../src/assets/acp-provider-icons"; +import { expect, test, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { openNewWorkspaceComposer } from "../support/helpers/new-workspace"; +import { openLaunchMenu } from "../support/helpers/new-workspace-launch"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; + +// Harmless commands: this journey opens the profile menu without launching them. +test.use({ + e2eDaemonConfig: { + version: 1, + daemon: { + terminalProfiles: [ + { id: "antigravity", name: "Antigravity artwork", command: "/bin/sh", icon: "agy" }, + { id: "cursor", name: "Cursor artwork", command: "/bin/sh", icon: "cursor" }, + { id: "unknown", name: "Unknown artwork", command: "/bin/sh", icon: "unknown-icon" }, + ], + }, + }, +}); + +function profileIcon(page: Page, name: string) { + return page.getByRole("menuitem", { name: new RegExp(`^${name} artwork`) }).locator("svg"); +} + +async function expectBundledArtwork(page: Page, name: string, artwork: string) { + const icon = profileIcon(page, name); + await expect(icon).toBeVisible(); + 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")!); + }, artwork); + 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 expect(icon).toHaveAttribute("width", "14"); + await expect(icon).toHaveAttribute("height", "14"); + const terminalColor = await page + .getByRole("menuitem", { name: "Terminal", exact: true }) + .locator("svg") + .evaluate((element) => getComputedStyle(element).stroke); + await expect(icon.locator("path").first()).toHaveCSS("fill", terminalColor); +} + +async function expectUnknownArtworkFallback(page: Page) { + const icon = profileIcon(page, "Unknown"); + await expect(icon).toBeVisible(); + await expect(icon.locator('path[d="M12 8V4H8"]')).toHaveCount(1); +} + +test("terminal profiles show bundled artwork while unknown icons keep the Bot fallback", async ({ + page, +}) => { + const workspace = await seedWorkspace({ repoPrefix: "terminal-icons-" }); + try { + await gotoAppShell(page); + await waitForSidebarHydration(page); + await openNewWorkspaceComposer(page, { + projectKey: workspace.projectKey, + projectDisplayName: workspace.projectDisplayName, + }); + await openLaunchMenu(page); + await expectBundledArtwork(page, "Antigravity", ACP_PROVIDER_ICON_SVGS.agy); + await expectBundledArtwork(page, "Cursor", ACP_PROVIDER_ICON_SVGS.cursor); + await expectUnknownArtworkFallback(page); + } finally { + await workspace.cleanup(); + } +}); diff --git a/packages/app/src/components/provider-icons.ts b/packages/app/src/components/provider-icons.ts index fec4003705..883b4820f7 100644 --- a/packages/app/src/components/provider-icons.ts +++ b/packages/app/src/components/provider-icons.ts @@ -3,6 +3,7 @@ import { providerSnapshotIcons } from "@/data/provider-icons"; import { Bot, PackagePlus } 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 { ClaudeIcon } from "@/components/icons/claude-icon"; import { CodexIcon } from "@/components/icons/codex-icon"; import { CopilotIcon } from "@/components/icons/copilot-icon"; @@ -10,6 +11,7 @@ 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"; @@ -31,10 +33,17 @@ const BUILTIN_PROVIDER_ICONS: Record = { pi: PiIcon as unknown as ProviderIconComponent, }; -const CATALOG_ICON_SVGS = new Map( +const CATALOG_ICON_SVGS = new Map( 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)[name]; + if (iconSvg) { + CATALOG_ICON_SVGS.set(name, iconSvg); + } +} + const catalogIconComponents = new Map(); const snapshotIconComponents = new Map();