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 { 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<CodexAccessState | null>(null); const [checking, setChecking] = useState(false); const [error, setError] = useState<string | null>(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<string | null>(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 ( <div className="flex flex-1 flex-col gap-6"> <p className="text-[13px] text-muted-foreground">Checking this machine for Codex…</p> </div> ); } if (plugin === undefined) { return ( <div className="flex flex-1 flex-col gap-6"> <p className="text-[13px] text-muted-foreground"> This Codex plugin was not found on this machine. </p> <FloatActions> <Button type="button" variant="ghost" onClick={() => props.onCancel()}> Back </Button> </FloatActions> </div> ); } return ( <div className="flex flex-1 flex-col gap-6"> {/* Mirrors the plugin's own page in Codex: its icon, display name, tagline, and long description, all read from the local install. */} <div className="flex flex-col gap-4"> {/* 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. */} <img src={plugin.icon ?? plugin.fallbackIcon ?? "https://integrations.sh/logo/openai.com"} alt="" className="size-16 rounded-2xl" /> <div className="min-w-0"> <div className="flex items-baseline gap-2"> <h1 className="text-xl font-semibold text-foreground"> {plugin.displayName ?? plugin.name} </h1> <span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground"> Codex plugin </span> </div> <p className="mt-1 text-[13px] text-muted-foreground"> {plugin.tagline ?? plugin.summary} </p> </div> {plugin.description !== undefined && ( <p className="max-w-prose text-[13px] leading-relaxed text-muted-foreground"> {plugin.description} </p> )} </div> <div className="flex flex-col gap-1 rounded-lg border border-border px-3 py-2.5"> <div className="flex items-baseline justify-between gap-3"> <span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground"> Status </span> <span className="font-mono text-[11px] text-muted-foreground"> {added ? "Added" : plugin.available ? "Ready" : "Not installed on this Mac"} </span> </div> {!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. <ol className="mt-1 flex list-none flex-col gap-1.5"> {plugin.setupHint.split("\n").map((step) => ( <li key={step} className="text-[12px] leading-relaxed text-muted-foreground"> {step} </li> ))} </ol> )} {plugin.available && !added && ( <p className="text-[12px] text-muted-foreground"> Runs the plugin from your Codex install. Nothing is downloaded. </p> )} </div> {/* 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 && ( <div className="flex flex-col gap-2 rounded-lg border border-border px-3 py-2.5"> <span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground"> macOS access </span> <p className="text-[12px] text-muted-foreground"> {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."} </p> <div className="flex items-center gap-3"> <span className={ access !== null && access.status !== "ok" ? "text-[12px] text-destructive" : "text-[12px] text-muted-foreground" } > {checking ? "Checking…" : access === null ? "" : access.status === "ok" ? "Allowed — macOS is letting this through." : (access.message ?? "Blocked.")} </span> {access !== null && access.status !== "ok" && !checking && ( <Button type="button" variant="secondary" onClick={() => void handleCheck()}> Check again </Button> )} </div> <ul className="flex flex-col gap-1.5"> {permissions.map((permission) => ( <li key={permission.id} className="flex items-baseline justify-between gap-3"> <span className="text-[12px] text-muted-foreground"> <span className="text-foreground">{permission.label}</span> — {permission.why} </span> <a href={permission.settingsUrl} className="shrink-0 font-mono text-[11px] text-muted-foreground underline underline-offset-2 hover:text-foreground" > Open settings </a> </li> ))} </ul> </div> )} {/* 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 && ( <div className="flex items-center gap-3"> <p className="text-[12px] text-destructive"> {access?.message ?? "This plugin cannot run yet."} </p> <Button type="button" variant="secondary" onClick={() => void handleCheck()}> Check again </Button> </div> )} {error !== null && <p className="text-[12px] text-destructive">{error}</p>} <FloatActions> <Button type="button" variant="ghost" onClick={() => props.onCancel()} disabled={adding}> Cancel </Button> {added ? ( <Button type="button" onClick={() => props.onComplete(plugin.slug)}> View integration </Button> ) : plugin.available ? ( <Button type="button" onClick={() => void handleAdd()} loading={adding} disabled={blocked || pending} > {pending ? "Checking access…" : "Add integration"} </Button> ) : ( <Button asChild> <a href={plugin.setupUrl ?? "https://openai.com/codex"} target="_blank" rel="noreferrer" > Install Codex </a> </Button> )} </FloatActions> </div> );}
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 { 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<CodexAccessState | null>(null);
const [checking, setChecking] = useState(false);
const [error, setError] = useState<string | null>(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<string | null>(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 (
<div className="flex flex-1 flex-col gap-6">
<p className="text-[13px] text-muted-foreground">Checking this machine for Codex…</p>
</div>
);
}
if (plugin === undefined) {
return (
<div className="flex flex-1 flex-col gap-6">
<p className="text-[13px] text-muted-foreground">
This Codex plugin was not found on this machine.
</p>
<FloatActions>
<Button type="button" variant="ghost" onClick={() => props.onCancel()}>
Back
</Button>
</FloatActions>
</div>
);
}
return (
<div className="flex flex-1 flex-col gap-6">
{/* Mirrors the plugin's own page in Codex: its icon, display name,
tagline, and long description, all read from the local install. */}
<div className="flex flex-col gap-4">
{/* 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. */}
<img
src={plugin.icon ?? plugin.fallbackIcon ?? "https://integrations.sh/logo/openai.com"}
alt=""
className="size-16 rounded-2xl"
/>
<div className="min-w-0">
<div className="flex items-baseline gap-2">
<h1 className="text-xl font-semibold text-foreground">
{plugin.displayName ?? plugin.name}
</h1>
<span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
Codex plugin
</span>
</div>
<p className="mt-1 text-[13px] text-muted-foreground">
{plugin.tagline ?? plugin.summary}
</p>
</div>
{plugin.description !== undefined && (
<p className="max-w-prose text-[13px] leading-relaxed text-muted-foreground">
{plugin.description}
</p>
)}
</div>
<div className="flex flex-col gap-1 rounded-lg border border-border px-3 py-2.5">
<div className="flex items-baseline justify-between gap-3">
<span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
Status
</span>
<span className="font-mono text-[11px] text-muted-foreground">
{added ? "Added" : plugin.available ? "Ready" : "Not installed on this Mac"}
</span>
</div>
{!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.
<ol className="mt-1 flex list-none flex-col gap-1.5">
{plugin.setupHint.split("\n").map((step) => (
<li key={step} className="text-[12px] leading-relaxed text-muted-foreground">
{step}
</li>
))}
</ol>
)}
{plugin.available && !added && (
<p className="text-[12px] text-muted-foreground">
Runs the plugin from your Codex install. Nothing is downloaded.
</p>
)}
</div>
{/* 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 && (
<div className="flex flex-col gap-2 rounded-lg border border-border px-3 py-2.5">
<span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
macOS access
</span>
<p className="text-[12px] text-muted-foreground">
{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."}
</p>
<div className="flex items-center gap-3">
<span
className={
access !== null && access.status !== "ok"
? "text-[12px] text-destructive"
: "text-[12px] text-muted-foreground"
}
>
{checking
? "Checking…"
: access === null
? ""
: access.status === "ok"
? "Allowed — macOS is letting this through."
: (access.message ?? "Blocked.")}
</span>
{access !== null && access.status !== "ok" && !checking && (
<Button type="button" variant="secondary" onClick={() => void handleCheck()}>
Check again
</Button>
)}
</div>
<ul className="flex flex-col gap-1.5">
{permissions.map((permission) => (
<li key={permission.id} className="flex items-baseline justify-between gap-3">
<span className="text-[12px] text-muted-foreground">
<span className="text-foreground">{permission.label}</span> — {permission.why}
</span>
<a
href={permission.settingsUrl}
className="shrink-0 font-mono text-[11px] text-muted-foreground underline underline-offset-2 hover:text-foreground"
>
Open settings
</a>
</li>
))}
</ul>
</div>
)}
{/* 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 && (
<div className="flex items-center gap-3">
<p className="text-[12px] text-destructive">
{access?.message ?? "This plugin cannot run yet."}
</p>
<Button type="button" variant="secondary" onClick={() => void handleCheck()}>
Check again
</Button>
</div>
)}
{error !== null && <p className="text-[12px] text-destructive">{error}</p>}
<FloatActions>
<Button type="button" variant="ghost" onClick={() => props.onCancel()} disabled={adding}>
Cancel
</Button>
{added ? (
<Button type="button" onClick={() => props.onComplete(plugin.slug)}>
View integration
</Button>
) : plugin.available ? (
<Button
type="button"
onClick={() => void handleAdd()}
loading={adding}
disabled={blocked || pending}
>
{pending ? "Checking access…" : "Add integration"}
</Button>
) : (
<Button asChild>
<a
href={plugin.setupUrl ?? "https://openai.com/codex"}
target="_blank"
rel="noreferrer"
>
Install Codex
</a>
</Button>
)}
</FloatActions>
</div>
);
}