Skip to content

Commit 7f2cfa4

Browse files
committed
Focused add screen and real icons for Codex plugins
1 parent f0f38a7 commit 7f2cfa4

6 files changed

Lines changed: 243 additions & 174 deletions

File tree

packages/plugins/mcp/src/api/group.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ const CodexPluginEntrySchema = Schema.Struct({
145145
cwd: Schema.optional(Schema.String),
146146
env: Schema.optional(StringMap),
147147
setupHint: Schema.optional(Schema.String),
148+
/** The plugin's own icon from its local install, as a data URI. */
149+
icon: Schema.optional(Schema.String),
148150
});
149151

150152
const ListCodexPluginsResponse = Schema.Struct({

packages/plugins/mcp/src/react/AddMcpIntegration.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields";
4141
import { McpRequestHeadersEditor } from "./McpRequestHeadersEditor";
4242
import { mcpHeadersFromRows, type McpHeaderRow } from "./request-headers";
4343
import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";
44-
import { CodexPluginsSection } from "./CodexPluginsSection";
44+
import CodexPluginAdd from "./CodexPluginAdd";
4545
import { parseStdioArgs } from "./stdio-fields";
4646
import { isProbableMcpEndpoint } from "./probe-url";
4747
import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode";
@@ -169,15 +169,14 @@ export default function AddMcpIntegration(props: {
169169
// Drop stdio presets when stdio is disabled — the caller should have
170170
// already filtered these out, but defence-in-depth.
171171
const preset = rawPreset?.transport === "stdio" && !allowStdio ? undefined : rawPreset;
172-
// A Codex plugin preset is a catalog pointer, not a spawn recipe: it opens
173-
// the stdio tab and highlights the matching Codex-plugins card (which
174-
// carries the server-resolved command and availability) instead of
175-
// prefilling the manual form.
172+
// A Codex plugin preset is a catalog pointer, not a spawn recipe: it gets
173+
// its own focused add screen (rendered below, before the generic form),
174+
// fed by the server-side scanner.
176175
const isCodexPreset = isCodexPresetId(preset?.id);
177176
const isStdioPreset = preset?.transport === "stdio" && !isCodexPreset;
178177

179178
const [transport, setTransport] = useState<"remote" | "stdio">(
180-
(isStdioPreset || isCodexPreset) && allowStdio ? "stdio" : "remote",
179+
isStdioPreset && allowStdio ? "stdio" : "remote",
181180
);
182181

183182
// --- Stdio state ---
@@ -398,6 +397,18 @@ export default function AddMcpIntegration(props: {
398397

399398
// ---- Render ----
400399

400+
// Placed after every hook so the hook order is identical on all renders;
401+
// `isCodexPreset` is fixed for the component's lifetime (route search param).
402+
if (isCodexPreset && preset) {
403+
return (
404+
<CodexPluginAdd
405+
presetId={preset.id}
406+
onComplete={props.onComplete}
407+
onCancel={props.onCancel}
408+
/>
409+
);
410+
}
411+
401412
return (
402413
<div className="flex flex-1 flex-col gap-6">
403414
<div>
@@ -525,13 +536,6 @@ export default function AddMcpIntegration(props: {
525536
</>
526537
) : (
527538
<>
528-
{/* Locally installed Codex plugins — one-click presets, with an
529-
install hint for entries whose binaries are missing. */}
530-
<CodexPluginsSection
531-
onComplete={(slug) => props.onComplete(slug)}
532-
{...(isCodexPreset && preset ? { highlightId: preset.id } : {})}
533-
/>
534-
535539
{/* Stdio form */}
536540
<CardStack>
537541
<CardStackContent className="border-t-0">
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { useState } from "react";
2+
import { useAtomValue, useAtomSet } from "@effect/atom-react";
3+
import * as Exit from "effect/Exit";
4+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5+
6+
import { Button } from "@executor-js/react/components/button";
7+
import { FloatActions } from "@executor-js/react/components/float-actions";
8+
import { integrationsOptimisticAtom } from "@executor-js/react/api/atoms";
9+
import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
10+
import { addIntegrationErrorMessage } from "@executor-js/react/lib/integration-add";
11+
12+
import { addMcpServer, codexPluginsAtom } from "./atoms";
13+
14+
// ---------------------------------------------------------------------------
15+
// Focused add screen for one Codex plugin, reached from its catalog preset
16+
// (e.g. searching "imessage" in the connect dialog). The preset is only a
17+
// pointer; everything shown here — icon, availability, spawn recipe — comes
18+
// from the server-side scanner reading the user's local Codex install. One
19+
// primary action: Add. No transport toggle, no manual command form.
20+
// ---------------------------------------------------------------------------
21+
22+
export default function CodexPluginAdd(props: {
23+
readonly presetId: string;
24+
readonly onComplete: (slug?: string) => void;
25+
readonly onCancel: () => void;
26+
}) {
27+
const pluginsResult = useAtomValue(codexPluginsAtom);
28+
const integrationsResult = useAtomValue(integrationsOptimisticAtom);
29+
const doAddServer = useAtomSet(addMcpServer, { mode: "promiseExit" });
30+
31+
const [adding, setAdding] = useState(false);
32+
const [error, setError] = useState<string | null>(null);
33+
34+
const plugin = AsyncResult.isSuccess(pluginsResult)
35+
? pluginsResult.value.plugins.find((entry) => entry.id === props.presetId)
36+
: undefined;
37+
38+
const added =
39+
plugin !== undefined &&
40+
AsyncResult.isSuccess(integrationsResult) &&
41+
integrationsResult.value.some((integration) => String(integration.slug) === plugin.slug);
42+
43+
const handleAdd = async () => {
44+
if (plugin === undefined) return;
45+
setAdding(true);
46+
setError(null);
47+
const exit = await doAddServer({
48+
payload: {
49+
transport: "stdio" as const,
50+
name: plugin.name,
51+
slug: plugin.slug,
52+
description: plugin.summary,
53+
command: plugin.command,
54+
args: [...plugin.args],
55+
...(plugin.cwd !== undefined ? { cwd: plugin.cwd } : {}),
56+
...(plugin.env !== undefined ? { env: { ...plugin.env } } : {}),
57+
},
58+
reactivityKeys: integrationWriteKeys,
59+
});
60+
if (Exit.isFailure(exit)) {
61+
setError(addIntegrationErrorMessage(exit, plugin.slug, "Failed to add plugin"));
62+
setAdding(false);
63+
return;
64+
}
65+
props.onComplete(exit.value.slug);
66+
};
67+
68+
if (!AsyncResult.isSuccess(pluginsResult)) {
69+
return (
70+
<div className="flex flex-1 flex-col gap-6">
71+
<p className="text-[13px] text-muted-foreground">Checking this machine for Codex…</p>
72+
</div>
73+
);
74+
}
75+
76+
if (plugin === undefined) {
77+
return (
78+
<div className="flex flex-1 flex-col gap-6">
79+
<p className="text-[13px] text-muted-foreground">
80+
This Codex plugin was not found on this machine.
81+
</p>
82+
<FloatActions>
83+
<Button type="button" variant="ghost" onClick={() => props.onCancel()}>
84+
Back
85+
</Button>
86+
</FloatActions>
87+
</div>
88+
);
89+
}
90+
91+
return (
92+
<div className="flex flex-1 flex-col gap-6">
93+
<div className="flex items-start gap-4">
94+
{plugin.icon !== undefined && (
95+
<img
96+
src={plugin.icon}
97+
alt=""
98+
className="mt-0.5 size-12 shrink-0 rounded-xl border border-border"
99+
/>
100+
)}
101+
<div className="min-w-0">
102+
<h1 className="text-xl font-semibold text-foreground">{plugin.name}</h1>
103+
<p className="mt-1 text-[13px] text-muted-foreground">{plugin.summary}</p>
104+
</div>
105+
</div>
106+
107+
<div className="flex flex-col gap-1 rounded-lg border border-border px-3 py-2.5">
108+
<div className="flex items-baseline justify-between gap-3">
109+
<span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
110+
Status
111+
</span>
112+
<span className="font-mono text-[11px] text-muted-foreground">
113+
{added ? "Added" : plugin.available ? "Ready" : "Requires Codex"}
114+
</span>
115+
</div>
116+
{!plugin.available && plugin.setupHint !== undefined && (
117+
<p className="text-[12px] text-muted-foreground">{plugin.setupHint}</p>
118+
)}
119+
{plugin.available && !added && (
120+
<p className="text-[12px] text-muted-foreground">
121+
Runs the plugin from your Codex install. Nothing is downloaded.
122+
</p>
123+
)}
124+
</div>
125+
126+
{error !== null && <p className="text-[12px] text-destructive">{error}</p>}
127+
128+
<FloatActions>
129+
<Button type="button" variant="ghost" onClick={() => props.onCancel()} disabled={adding}>
130+
Cancel
131+
</Button>
132+
{added ? (
133+
<Button type="button" onClick={() => props.onComplete(plugin.slug)}>
134+
View integration
135+
</Button>
136+
) : (
137+
<Button
138+
type="button"
139+
onClick={() => void handleAdd()}
140+
disabled={!plugin.available}
141+
loading={adding}
142+
>
143+
Add integration
144+
</Button>
145+
)}
146+
</FloatActions>
147+
</div>
148+
);
149+
}

packages/plugins/mcp/src/react/CodexPluginsSection.tsx

Lines changed: 0 additions & 144 deletions
This file was deleted.

0 commit comments

Comments
 (0)