Unified · Split
| @@ -163,6 +163,8 @@ const CodexPluginEntrySchema = Schema.Struct({ · 162 unmodified lines | |||
| 163 | }), | 163 | }), |
| 164 | ), | 164 | ), |
| 165 | setupHint: Schema.optional(Schema.String), | 165 | setupHint: Schema.optional(Schema.String), |
| 166 | setupUrl: Schema.optional(Schema.String), | ||
| 167 | fallbackIcon: Schema.optional(Schema.String), | ||
| 166 | /** The plugin's own icon from its local install, as a data URI. */ | 168 | /** The plugin's own icon from its local install, as a data URI. */ |
| 167 | icon: Schema.optional(Schema.String), | 169 | icon: Schema.optional(Schema.String), |
| 168 | /** The plugin's own display metadata from its local manifest. */ | 170 | /** The plugin's own display metadata from its local manifest. */ |
| @@ -98,7 +98,7 @@ export default function CodexPluginAdd(props: { · 97 unmodified lines | |||
| 98 | one the card still identifies its provider rather than showing a | 98 | one the card still identifies its provider rather than showing a |
| 99 | gap, which matters most on the machines that have no install. */} | 99 | gap, which matters most on the machines that have no install. */} |
| 100 | <img | 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 | alt="" | 102 | alt="" |
| 103 | className="size-16 rounded-2xl" | 103 | className="size-16 rounded-2xl" |
| 104 | /> | 104 | /> |
| @@ -128,7 +128,7 @@ export default function CodexPluginAdd(props: { · 23 unmodified lines | |||
| 128 | Status | 128 | Status |
| 129 | </span> | 129 | </span> |
| 130 | <span className="font-mono text-[11px] text-muted-foreground"> | 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 | </span> | 132 | </span> |
| 133 | </div> | 133 | </div> |
| 134 | {!plugin.available && plugin.setupHint !== undefined && ( | 134 | {!plugin.available && plugin.setupHint !== undefined && ( |
| @@ -159,15 +159,20 @@ export default function CodexPluginAdd(props: { · 24 unmodified lines | |||
| 159 | <Button type="button" onClick={() => props.onComplete(plugin.slug)}> | 159 | <Button type="button" onClick={() => props.onComplete(plugin.slug)}> |
| 160 | View integration | 160 | View integration |
| 161 | </Button> | 161 | </Button> |
| 162 | ) : ( | 162 | ) : plugin.available ? ( |
| 163 | <Button | 163 | <Button type="button" onClick={() => void handleAdd()} loading={adding}> |
| 164 | type="button" | ||
| 165 | onClick={() => void handleAdd()} | ||
| 166 | disabled={!plugin.available} | ||
| 167 | loading={adding} | ||
| 168 | > | ||
| 169 | Add integration | 164 | Add integration |
| 170 | </Button> | 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 | </FloatActions> | 177 | </FloatActions> |
| 173 | </div> | 178 | </div> |
| @@ -18,6 +18,10 @@ export interface CuratedCodexPlugin { · 17 unmodified lines | |||
| 18 | /** The MCP server name this plugin registers inside Codex — the `server` | 18 | /** The MCP server name this plugin registers inside Codex — the `server` |
| 19 | * the app-server bridge calls tools against. */ | 19 | * the app-server bridge calls tools against. */ |
| 20 | readonly server: string; | 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 | /** Present when the plugin has no MCP server of its own and its API is | 25 | /** Present when the plugin has no MCP server of its own and its API is |
| 22 | * projected onto another one. Computer Use and Chrome both ship as | 26 | * projected onto another one. Computer Use and Chrome both ship as |
| 23 | * skills/`node-repl` content: Codex never starts a server for either, and | 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 | .map((step, index) => `${index + 1}. ${step}`) | 72 | .map((step, index) => `${index + 1}. ${step}`) |
| 69 | .join("\n"); | 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 | export const CURATED_CODEX_PLUGINS: readonly CuratedCodexPlugin[] = [ | 85 | export const CURATED_CODEX_PLUGINS: readonly CuratedCodexPlugin[] = [ |
| 72 | // Names are exactly the plugins' own displayNames — nothing invented, no | 86 | // Names are exactly the plugins' own displayNames — nothing invented, no |
| 73 | // provenance suffix. Codex provenance shows in the summaries and on the | 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 | pluginName: "computer-use", | 102 | pluginName: "computer-use", |
| 89 | name: "Computer Use", | 103 | name: "Computer Use", |
| 90 | slug: "codex_computer_use", | 104 | slug: "codex_computer_use", |
| 105 | publicIcon: "https://learn.chatgpt.com/images/codex/icons/computer-use-plugin-icon.png", | ||
| 91 | requires: "computer-use-app", | 106 | requires: "computer-use-app", |
| 92 | server: "node_repl", | 107 | server: "node_repl", |
| 93 | surface: "sky", | 108 | surface: "sky", |
| @@ -101,6 +116,7 @@ export const CURATED_CODEX_PLUGINS: readonly CuratedCodexPlugin[] = [ · 7 unmodified lines | |||
| 101 | pluginName: "chrome", | 116 | pluginName: "chrome", |
| 102 | name: "Chrome", | 117 | name: "Chrome", |
| 103 | slug: "codex_chrome", | 118 | slug: "codex_chrome", |
| 119 | publicIcon: "https://learn.chatgpt.com/images/codex/icons/chrome-production-large.png", | ||
| 104 | server: "node_repl", | 120 | server: "node_repl", |
| 105 | surface: "browser", | 121 | surface: "browser", |
| 106 | requires: "chrome-plugin", | 122 | requires: "chrome-plugin", |
| @@ -177,6 +177,12 @@ describe("scanCodexPlugins", () => { · 176 unmodified lines | |||
| 177 | // Each card's steps are about ITS requirement: Chrome needs the browser | 177 | // Each card's steps are about ITS requirement: Chrome needs the browser |
| 178 | // extension, which no other card mentions. | 178 | // extension, which no other card mentions. |
| 179 | expect(byId.get("codex-chrome")?.setupHint).toContain("browser extension"); | 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 | expect(byId.get("codex-messages")?.setupHint).not.toContain("browser extension"); | 186 | expect(byId.get("codex-messages")?.setupHint).not.toContain("browser extension"); |
| 181 | // And the steps are numbered and name the plugin the person clicked. | 187 | // And the steps are numbered and name the plugin the person clicked. |
| 182 | expect(byId.get("codex-messages")?.setupHint).toContain("1. Install the Codex app"); | 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 | import { Option, Schema } from "effect"; | 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 | export interface CodexPluginEntry { | 30 | export interface CodexPluginEntry { |
| 26 | /** Stable card id, e.g. `codex-messages`. */ | 31 | /** Stable card id, e.g. `codex-messages`. */ |
| @@ -46,8 +51,11 @@ export interface CodexPluginEntry { · 19 unmodified lines | |||
| 46 | readonly surface?: "sky" | "browser"; | 51 | readonly surface?: "sky" | "browser"; |
| 47 | readonly modulePath?: string; | 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 | readonly setupHint?: string; | 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 | /** The plugin's own icon from its local install, as a data URI. Read at | 59 | /** The plugin's own icon from its local install, as a data URI. Read at |
| 52 | * runtime from the user's disk — never shipped with executor. */ | 60 | * runtime from the user's disk — never shipped with executor. */ |
| 53 | readonly icon?: string; | 61 | readonly icon?: string; |
| @@ -416,7 +424,10 @@ export const scanCodexPlugins = (options?: { · 362 unmodified lines | |||
| 416 | ...(entry.surface === undefined ? {} : { surface: entry.surface }), | 424 | ...(entry.surface === undefined ? {} : { surface: entry.surface }), |
| 417 | ...(entry.surface === "browser" ? { modulePath: browserClient } : {}), | 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 | ...display, | 431 | ...display, |
| 421 | }; | 432 | }; |
| 422 | }); | 433 | }); |
| @@ -49,7 +49,7 @@ const codexPluginPresets: readonly McpStdioPreset[] = CURATED_CODEX_PLUGINS.map( · 48 unmodified lines | |||
| 49 | // without Codex has none to read. Fall back to the provider's mark from the | 49 | // without Codex has none to read. Fall back to the provider's mark from the |
| 50 | // same logo service every other preset uses, rather than vendoring OpenAI's | 50 | // same logo service every other preset uses, rather than vendoring OpenAI's |
| 51 | // artwork into this repo. | 51 | // artwork into this repo. |
| 52 | fallbackIcon: "https://integrations.sh/logo/openai.com", | 52 | fallbackIcon: plugin.publicIcon ?? "https://integrations.sh/logo/openai.com", |
| 53 | family: "codex", | 53 | family: "codex", |
| 54 | defaultSlug: plugin.slug, | 54 | defaultSlug: plugin.slug, |
| 55 | transport: "stdio", | 55 | transport: "stdio", |