import { useEffect, useRef, useState } from "react"; import { useAtomValue, useAtomSet } from "@effect/atom-react"; import * as Exit from "effect/Exit"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { Button } from "@executor-js/react/components/button"; import { FloatActions } from "@executor-js/react/components/float-actions"; import { IntegrationFavicon } from "@executor-js/react/components/integration-favicon"; import { integrationsOptimisticAtom } from "@executor-js/react/api/atoms"; import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { addIntegrationErrorMessage } from "@executor-js/react/lib/integration-add"; import { addMcpServer, checkCodexPluginAccess, codexPluginsAtom } from "./atoms"; import { accessBlocked, accessPending, type CodexAccessState } from "./codex-access-gate"; import { CODEX_PERMISSIONS } from "../sdk/codex-permissions"; // --------------------------------------------------------------------------- // Focused add screen for one Codex plugin, reached from its catalog preset // (e.g. searching "imessage" in the connect dialog). The preset is only a // pointer; everything shown here — icon, availability, spawn recipe — comes // from the server-side scanner reading the user's local Codex install. One // primary action: Add. No transport toggle, no manual command form. // --------------------------------------------------------------------------- export default function CodexPluginAdd(props: { readonly presetId: string; readonly onComplete: (slug?: string) => void; readonly onCancel: () => void; }) { const pluginsResult = useAtomValue(codexPluginsAtom); const integrationsResult = useAtomValue(integrationsOptimisticAtom); const doAddServer = useAtomSet(addMcpServer, { mode: "promiseExit" }); const doCheckAccess = useAtomSet(checkCodexPluginAccess, { mode: "promiseExit" }); const [adding, setAdding] = useState(false); const [access, setAccess] = useState(null); const [checking, setChecking] = useState(false); const [error, setError] = useState(null); const plugin = AsyncResult.isSuccess(pluginsResult) ? pluginsResult.value.plugins.find((entry) => entry.id === props.presetId) : undefined; const permissions = CODEX_PERMISSIONS[props.presetId] ?? []; const added = plugin !== undefined && AsyncResult.isSuccess(integrationsResult) && integrationsResult.value.some((integration) => String(integration.slug) === plugin.slug); // Adding is held back until the probe says the plugin can actually run: an // integration added while macOS is blocking it looks connected and fails on // its first real call, by which point the person has left the one card that // explains the fix. const blocked = accessBlocked(access); const pending = accessPending({ checking, declaresPermissions: permissions.length > 0, access, }); const handleAdd = async () => { if (plugin === undefined) return; setAdding(true); setError(null); const exit = await doAddServer({ payload: { transport: "stdio" as const, name: plugin.name, slug: plugin.slug, description: plugin.summary, command: plugin.command, args: [...plugin.args], ...(plugin.cwd !== undefined ? { cwd: plugin.cwd } : {}), // As STATIC env, not `env`: these are machine-derived paths the // scanner already resolved, so they must not become credentials the // person is asked to type. Sent this way the integration declares no // auth, and its connection is created for them. ...(plugin.env !== undefined ? { staticEnv: { ...plugin.env } } : {}), ...(plugin.appServer !== undefined ? { appServer: { ...plugin.appServer } } : {}), }, reactivityKeys: integrationWriteKeys, }); if (Exit.isFailure(exit)) { setError(addIntegrationErrorMessage(exit, plugin.slug, "Failed to add plugin")); setAdding(false); return; } props.onComplete(exit.value.slug); }; // Checked on open rather than on a button, so the card states where you // stand before you commit to anything. It runs the plugin's read-only probe // once per card: macOS offers no way to READ another app's privacy // decisions, so trying it is the only way to know. const checkedFor = useRef(null); useEffect(() => { if (!plugin?.available) return; if (checkedFor.current === props.presetId) return; checkedFor.current = props.presetId; void handleCheck(); // `handleCheck` is stable for a given preset; re-running on its identity // would re-probe on every render. // eslint-disable-next-line react-hooks/exhaustive-deps }, [plugin?.available, props.presetId]); const handleCheck = async () => { setChecking(true); setAccess(null); // Runs the plugin's own read-only probe down the real path. When macOS has // not asked yet, THIS is what makes it ask — so the check doubles as the // grant flow. // Reads nothing and writes nothing, so no cache is invalidated by it. const exit = await doCheckAccess({ params: { id: props.presetId }, reactivityKeys: [] }); setAccess( Exit.isSuccess(exit) ? (exit.value as CodexAccessState) : { status: "blocked", message: "Could not reach the plugin." }, ); setChecking(false); }; if (!AsyncResult.isSuccess(pluginsResult)) { return (

Checking this machine for Codex…

); } if (plugin === undefined) { return (

This Codex plugin was not found on this machine.

); } return (
{/* Mirrors the plugin's own page in Codex: its icon, display name, tagline, and long description, all read from the local install. */}
{/* The plugin's own icon comes from the local Codex install; without one the card still identifies its provider rather than showing a gap, which matters most on the machines that have no install. */}

{plugin.displayName ?? plugin.name}

Codex plugin

{plugin.tagline ?? plugin.summary}

{plugin.description !== undefined && (

{plugin.description}

)}
Status {added ? "Added" : plugin.available ? "Ready" : "Not installed on this Mac"}
{!plugin.available && plugin.setupHint !== undefined && ( // The hint arrives as numbered lines; render them as the list they // are, so the reader sees an ordered path rather than a paragraph.
    {plugin.setupHint.split("\n").map((step) => (
  1. {step}
  2. ))}
)} {plugin.available && !added && (

Runs the plugin from your Codex install. Nothing is downloaded.

)}
{/* Stated up front, not discovered on first failure: macOS asks for these ONCE, and a dismissed prompt never returns — after that the plugin fails with an error the user cannot act on. Each row links straight to the pane that holds the switch. */} {permissions.length > 0 && (
macOS access

{blocked ? "This has to be on before the plugin can be added — turn it on below, then check again." : "macOS asks the first time this runs. If you miss the prompt, it will not ask again — enable it here."}

{checking ? "Checking…" : access === null ? "" : access.status === "ok" ? "Allowed — macOS is letting this through." : (access.message ?? "Blocked.")} {access !== null && access.status !== "ok" && !checking && ( )}
    {permissions.map((permission) => (
  • {permission.label} — {permission.why} Open settings
  • ))}
)} {/* A block on a plugin that declares no macOS access has no panel above to carry it, so state the reason next to the action it is holding. */} {blocked && permissions.length === 0 && (

{access?.message ?? "This plugin cannot run yet."}

)} {error !== null &&

{error}

} {added ? ( ) : plugin.available ? ( ) : ( )}
); }