History

Use published plugin marks and link the install from the add screen

Rhys Sullivan committed 2 months ago

Commit a87180f1 parent ff496ec6 files · +53 −13Browse files

Unified · Split

6 files
  1. packages/plugins/mcp/src/api/group.ts +2 −0
  2. packages/plugins/mcp/src/react/CodexPluginAdd.tsx +14 −9
  3. packages/plugins/mcp/src/sdk/codex-plugin-presets.ts +16 −0
  4. packages/plugins/mcp/src/sdk/codex-plugins.test.ts +6 −0
  5. packages/plugins/mcp/src/sdk/codex-plugins.ts +14 −3
  6. packages/plugins/mcp/src/sdk/presets.ts +1 −1
packages/plugins/mcp/src/api/group.ts+2−0
@@ -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. */
packages/plugins/mcp/src/react/CodexPluginAdd.tsx+14−9
@@ -98,7 +98,7 @@ export default function CodexPluginAdd(props: { · 97 unmodified lines
98 one the card still identifies its provider rather than showing a98 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 <img100 <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 Status128 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 integration160 View integration
161 </Button>161 </Button>
162 ) : (162 ) : plugin.available ? (
163 <Button163 <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 integration164 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>
packages/plugins/mcp/src/sdk/codex-plugin-presets.ts+16−0
@@ -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 is25 /** 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 as26 * projected onto another one. Computer Use and Chrome both ship as
23 * skills/`node-repl` content: Codex never starts a server for either, and27 * 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. */
80export const setupUrl = (requires: CuratedCodexPlugin["requires"]): string =>
81 requires === "chrome-plugin"
82 ? "https://learn.chatgpt.com/docs/computer-use"
83 : "https://openai.com/codex";
84
71export const CURATED_CODEX_PLUGINS: readonly CuratedCodexPlugin[] = [85export const CURATED_CODEX_PLUGINS: readonly CuratedCodexPlugin[] = [
72 // Names are exactly the plugins' own displayNames — nothing invented, no86 // Names are exactly the plugins' own displayNames — nothing invented, no
73 // provenance suffix. Codex provenance shows in the summaries and on the87 // 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",
packages/plugins/mcp/src/sdk/codex-plugins.test.ts+6−0
@@ -177,6 +177,12 @@ describe("scanCodexPlugins", () => { · 176 unmodified lines
177 // Each card's steps are about ITS requirement: Chrome needs the browser177 // 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");
packages/plugins/mcp/src/sdk/codex-plugins.ts+14−3
@@ -20,7 +20,12 @@ import * as path from "node:path"; · 19 unmodified lines
20 20
21import { Option, Schema } from "effect";21import { Option, Schema } from "effect";
22 22
23import { CURATED_CODEX_PLUGINS, setupHint, type CuratedCodexPlugin } from "./codex-plugin-presets";23import {
24 CURATED_CODEX_PLUGINS,
25 setupHint,
26 setupUrl,
27 type CuratedCodexPlugin,
28} from "./codex-plugin-presets";
24 29
25export interface CodexPluginEntry {30export 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 at59 /** 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 });
packages/plugins/mcp/src/sdk/presets.ts+1−1
@@ -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 the49 // 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's50 // 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",