|
| 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 | +} |
0 commit comments