Unified · Split
| @@ -163,6 +163,8 @@ const CodexPluginEntrySchema = Schema.Struct({ · 162 unmodified lines | ||
| 163 | 163 | }), |
| 164 | 164 | ), |
| 165 | 165 | setupHint: Schema.optional(Schema.String), |
| 166 | + setupUrl: Schema.optional(Schema.String), | |
| 167 | + fallbackIcon: Schema.optional(Schema.String), | |
| 166 | 168 | /** The plugin's own icon from its local install, as a data URI. */ |
| 167 | 169 | icon: Schema.optional(Schema.String), |
| 168 | 170 | /** The plugin's own display metadata from its local manifest. */ |
| @@ -98,7 +98,7 @@ export default function CodexPluginAdd(props: { · 97 unmodified lines | ||
| 98 | 98 | one the card still identifies its provider rather than showing a |
| 99 | 99 | gap, which matters most on the machines that have no install. */} |
| 100 | 100 | <img |
| 101 | − src={plugin.icon ?? "https://integrations.sh/logo/openai.com"} | |
| 101 | + src={plugin.icon ?? plugin.fallbackIcon ?? "https://integrations.sh/logo/openai.com"} | |
| 102 | 102 | alt="" |
| 103 | 103 | className="size-16 rounded-2xl" |
| 104 | 104 | /> |
| @@ -128,7 +128,7 @@ export default function CodexPluginAdd(props: { · 23 unmodified lines | ||
| 128 | 128 | Status |
| 129 | 129 | </span> |
| 130 | 130 | <span className="font-mono text-[11px] text-muted-foreground"> |
| 131 | − {added ? "Added" : plugin.available ? "Ready" : "Requires Codex"} | |
| 131 | + {added ? "Added" : plugin.available ? "Ready" : "Not installed on this Mac"} | |
| 132 | 132 | </span> |
| 133 | 133 | </div> |
| 134 | 134 | {!plugin.available && plugin.setupHint !== undefined && ( |
| @@ -159,15 +159,20 @@ export default function CodexPluginAdd(props: { · 24 unmodified lines | ||
| 159 | 159 | <Button type="button" onClick={() => props.onComplete(plugin.slug)}> |
| 160 | 160 | View integration |
| 161 | 161 | </Button> |
| 162 | − ) : ( | |
| 163 | − <Button | |
| 164 | − type="button" | |
| 165 | − onClick={() => void handleAdd()} | |
| 166 | − disabled={!plugin.available} | |
| 167 | − loading={adding} | |
| 168 | − > | |
| 162 | + ) : plugin.available ? ( | |
| 163 | + <Button type="button" onClick={() => void handleAdd()} loading={adding}> | |
| 169 | 164 | Add integration |
| 170 | 165 | </Button> |
| 166 | + ) : ( | |
| 167 | + <Button asChild> | |
| 168 | + <a | |
| 169 | + href={plugin.setupUrl ?? "https://openai.com/codex"} | |
| 170 | + target="_blank" | |
| 171 | + rel="noreferrer" | |
| 172 | + > | |
| 173 | + Install Codex | |
| 174 | + </a> | |
| 175 | + </Button> | |
| 171 | 176 | )} |
| 172 | 177 | </FloatActions> |
| 173 | 178 | </div> |
| @@ -18,6 +18,10 @@ export interface CuratedCodexPlugin { · 17 unmodified lines | ||
| 18 | 18 | /** The MCP server name this plugin registers inside Codex — the `server` |
| 19 | 19 | * the app-server bridge calls tools against. */ |
| 20 | 20 | readonly server: string; |
| 21 | + /** A public image for this plugin, used when the machine-local icon cannot | |
| 22 | + * be read (i.e. Codex is not installed here). Only some plugins have one | |
| 23 | + * published; the rest fall back to the provider's mark. */ | |
| 24 | + readonly publicIcon?: string; | |
| 21 | 25 | /** Present when the plugin has no MCP server of its own and its API is |
| 22 | 26 | * projected onto another one. Computer Use and Chrome both ship as |
| 23 | 27 | * skills/`node-repl` content: Codex never starts a server for either, and |
| @@ -68,6 +72,16 @@ export const setupHint = (requires: CuratedCodexPlugin["requires"], name: string · 44 unmodified lines | ||
| 68 | 72 | .map((step, index) => `${index + 1}. ${step}`) |
| 69 | 73 | .join("\n"); |
| 70 | 74 | |
| 75 | +/** Where a person goes to get what a card needs. | |
| 76 | + * | |
| 77 | + * Linked rather than only described: a card that cannot be used is a dead end | |
| 78 | + * unless it hands over the next step. Chrome's own requirement is documented | |
| 79 | + * on the Computer Use page, so it points there instead of the app download. */ | |
| 80 | +export const setupUrl = (requires: CuratedCodexPlugin["requires"]): string => | |
| 81 | + requires === "chrome-plugin" | |
| 82 | + ? "https://learn.chatgpt.com/docs/computer-use" | |
| 83 | + : "https://openai.com/codex"; | |
| 84 | + | |
| 71 | 85 | export const CURATED_CODEX_PLUGINS: readonly CuratedCodexPlugin[] = [ |
| 72 | 86 | // Names are exactly the plugins' own displayNames — nothing invented, no |
| 73 | 87 | // provenance suffix. Codex provenance shows in the summaries and on the |
| @@ -88,6 +102,7 @@ export const CURATED_CODEX_PLUGINS: readonly CuratedCodexPlugin[] = [ · 14 unmodified lines | ||
| 88 | 102 | pluginName: "computer-use", |
| 89 | 103 | name: "Computer Use", |
| 90 | 104 | slug: "codex_computer_use", |
| 105 | + publicIcon: "https://learn.chatgpt.com/images/codex/icons/computer-use-plugin-icon.png", | |
| 91 | 106 | requires: "computer-use-app", |
| 92 | 107 | server: "node_repl", |
| 93 | 108 | surface: "sky", |
| @@ -101,6 +116,7 @@ export const CURATED_CODEX_PLUGINS: readonly CuratedCodexPlugin[] = [ · 7 unmodified lines | ||
| 101 | 116 | pluginName: "chrome", |
| 102 | 117 | name: "Chrome", |
| 103 | 118 | slug: "codex_chrome", |
| 119 | + publicIcon: "https://learn.chatgpt.com/images/codex/icons/chrome-production-large.png", | |
| 104 | 120 | server: "node_repl", |
| 105 | 121 | surface: "browser", |
| 106 | 122 | requires: "chrome-plugin", |
| @@ -177,6 +177,12 @@ describe("scanCodexPlugins", () => { · 176 unmodified lines | ||
| 177 | 177 | // Each card's steps are about ITS requirement: Chrome needs the browser |
| 178 | 178 | // extension, which no other card mentions. |
| 179 | 179 | expect(byId.get("codex-chrome")?.setupHint).toContain("browser extension"); |
| 180 | + // A card that cannot be used still hands over where to go, and shows the | |
| 181 | + // plugin's published mark rather than a gap. | |
| 182 | + expect(byId.get("codex-messages")?.setupUrl).toBe("https://openai.com/codex"); | |
| 183 | + expect(byId.get("codex-chrome")?.setupUrl).toContain("learn.chatgpt.com"); | |
| 184 | + expect(byId.get("codex-computer-use")?.fallbackIcon).toContain("computer-use-plugin-icon"); | |
| 185 | + expect(byId.get("codex-messages")?.fallbackIcon).toContain("integrations.sh"); | |
| 180 | 186 | expect(byId.get("codex-messages")?.setupHint).not.toContain("browser extension"); |
| 181 | 187 | // And the steps are numbered and name the plugin the person clicked. |
| 182 | 188 | expect(byId.get("codex-messages")?.setupHint).toContain("1. Install the Codex app"); |
| @@ -20,7 +20,12 @@ import * as path from "node:path"; · 19 unmodified lines | ||
| 20 | 20 | |
| 21 | 21 | import { Option, Schema } from "effect"; |
| 22 | 22 | |
| 23 | −import { CURATED_CODEX_PLUGINS, setupHint, type CuratedCodexPlugin } from "./codex-plugin-presets"; | |
| 23 | +import { | |
| 24 | + CURATED_CODEX_PLUGINS, | |
| 25 | + setupHint, | |
| 26 | + setupUrl, | |
| 27 | + type CuratedCodexPlugin, | |
| 28 | +} from "./codex-plugin-presets"; | |
| 24 | 29 | |
| 25 | 30 | export interface CodexPluginEntry { |
| 26 | 31 | /** Stable card id, e.g. `codex-messages`. */ |
| @@ -46,8 +51,11 @@ export interface CodexPluginEntry { · 19 unmodified lines | ||
| 46 | 51 | readonly surface?: "sky" | "browser"; |
| 47 | 52 | readonly modulePath?: string; |
| 48 | 53 | }; |
| 49 | − /** Shown when `available` is false. */ | |
| 54 | + /** Shown when `available` is false: the ordered steps, and where to go. */ | |
| 50 | 55 | readonly setupHint?: string; |
| 56 | + readonly setupUrl?: string; | |
| 57 | + /** Public image for this plugin, for machines with no local install. */ | |
| 58 | + readonly fallbackIcon?: string; | |
| 51 | 59 | /** The plugin's own icon from its local install, as a data URI. Read at |
| 52 | 60 | * runtime from the user's disk — never shipped with executor. */ |
| 53 | 61 | readonly icon?: string; |
| @@ -416,7 +424,10 @@ export const scanCodexPlugins = (options?: { · 362 unmodified lines | ||
| 416 | 424 | ...(entry.surface === undefined ? {} : { surface: entry.surface }), |
| 417 | 425 | ...(entry.surface === "browser" ? { modulePath: browserClient } : {}), |
| 418 | 426 | }, |
| 419 | − ...(available ? {} : { setupHint: setupHint(entry.requires, entry.name) }), | |
| 427 | + ...(available | |
| 428 | + ? {} | |
| 429 | + : { setupHint: setupHint(entry.requires, entry.name), setupUrl: setupUrl(entry.requires) }), | |
| 430 | + fallbackIcon: entry.publicIcon ?? "https://integrations.sh/logo/openai.com", | |
| 420 | 431 | ...display, |
| 421 | 432 | }; |
| 422 | 433 | }); |
| @@ -49,7 +49,7 @@ const codexPluginPresets: readonly McpStdioPreset[] = CURATED_CODEX_PLUGINS.map( · 48 unmodified lines | ||
| 49 | 49 | // without Codex has none to read. Fall back to the provider's mark from the |
| 50 | 50 | // same logo service every other preset uses, rather than vendoring OpenAI's |
| 51 | 51 | // artwork into this repo. |
| 52 | − fallbackIcon: "https://integrations.sh/logo/openai.com", | |
| 52 | + fallbackIcon: plugin.publicIcon ?? "https://integrations.sh/logo/openai.com", | |
| 53 | 53 | family: "codex", |
| 54 | 54 | defaultSlug: plugin.slug, |
| 55 | 55 | transport: "stdio", |