diff --git a/.changeset/integration-icon-unification.md b/.changeset/integration-icon-unification.md
new file mode 100644
index 0000000000..fd3fd263c1
--- /dev/null
+++ b/.changeset/integration-icon-unification.md
@@ -0,0 +1,12 @@
+---
+"executor": patch
+"@executor-js/plugin-mcp": patch
+---
+
+Show real icons on the integration browse page. The Codex plugin preset cards
+rendered a bare-letter avatar: the page fed the preset's authenticated
+`executor:` icon path into a raw ``, which can never load it, and never
+consulted the preset's public fallback image. Icon rendering is now unified on
+one component — explicit icon, then its fallback image, then the favicon
+derived from the integration's URL, then a neutral mark — used by the browse
+cards, the command palette, and the Codex plugin add screen alike.
diff --git a/e2e/local/codex-plugins.test.ts b/e2e/local/codex-plugins.test.ts
index 423e9a13ed..89597666f6 100644
--- a/e2e/local/codex-plugins.test.ts
+++ b/e2e/local/codex-plugins.test.ts
@@ -28,7 +28,7 @@ import { composePluginApi } from "@executor-js/api/server";
import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
import { scenario } from "../src/scenario";
-import { Cli, RunDir } from "../src/services";
+import { Browser, Cli, RunDir, Target } from "../src/services";
import { withLocalServer } from "./local-server";
const api = composePluginApi([mcpHttpPlugin()] as const);
@@ -109,7 +109,10 @@ scenario(
{ timeout: 300_000 },
Effect.gen(function* () {
const cli = yield* Cli;
+ const browser = yield* Browser;
const runDir = yield* RunDir;
+ const target = yield* Target;
+ const identity = yield* target.newIdentity();
const codexHome = makeCodexHome();
yield* withLocalServer(
@@ -142,6 +145,25 @@ scenario(
CODEX_HOME: codexHome,
});
}
+
+ yield* browser.session(identity, async ({ page, step }) => {
+ await step("Find Computer Use on the integration browse page", async () => {
+ await page.goto(server.url, { waitUntil: "domcontentloaded" });
+ await page.getByRole("link", { name: "Add integration" }).click();
+ await page
+ .getByRole("textbox", { name: "Search integrations, or paste a URL" })
+ .fill("Computer Use");
+
+ const card = page.getByTestId("preset-mcp-codex-computer-use");
+ await card.getByText("Computer Use MCP").waitFor({ timeout: 30_000 });
+ await card
+ .locator(
+ 'img[src="https://learn.chatgpt.com/images/codex/icons/computer-use-plugin-icon.png"]',
+ )
+ .waitFor({ timeout: 30_000 });
+ });
+ });
+
// Curated entries carry the app-server bridge recipe: `codex
// app-server`, the server name the bridge calls tools on, and the
// preset it came from — that last one is what lets a macOS refusal
diff --git a/packages/plugins/mcp/src/react/CodexPluginAdd.tsx b/packages/plugins/mcp/src/react/CodexPluginAdd.tsx
index 957e1d00f0..dbef98116a 100644
--- a/packages/plugins/mcp/src/react/CodexPluginAdd.tsx
+++ b/packages/plugins/mcp/src/react/CodexPluginAdd.tsx
@@ -5,6 +5,7 @@ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import { Button } from "@executor-js/react/components/button";
import { FloatActions } from "@executor-js/react/components/float-actions";
+import { IntegrationFavicon } from "@executor-js/react/components/integration-favicon";
import { integrationsOptimisticAtom } from "@executor-js/react/api/atoms";
import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
import { addIntegrationErrorMessage } from "@executor-js/react/lib/integration-add";
@@ -150,10 +151,11 @@ export default function CodexPluginAdd(props: {
{/* The plugin's own icon comes from the local Codex install; without
one the card still identifies its provider rather than showing a
gap, which matters most on the machines that have no install. */}
-