import { t } from "../../lib/console-text"; import { attachSessionCapability, prepareSessionCapabilityAccess, applySessionCapabilityAccess, type SessionCapabilityAccessPlan, } from "./attach-session-capability"; import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { SessionMcpCapabilityCard } from "../../../components/session-mcp-capability-card"; import type { AuthNeededItem } from "../../../timeline"; import { CheckIcon, Loader2Icon } from "lucide-react"; import { useAppContext } from "../../app-context"; import { CodexSubscriptionsSurface } from "../../lib/app-surfaces"; import { Button } from "../../../ui/button"; import { Notice } from "../../../ui/notice"; import { SessionCapabilityFrame } from "../../../components/session-capability-frame"; import { capabilityLogoSource } from "./capability-logo-source"; import { DetailBody, type ConnectAction } from "./capability-detail-sheet"; import { performCapabilityAction } from "./perform-capability-action"; import { useCapabilitiesCatalog } from "./use-capabilities-catalog"; import { SessionCustomMcpCard } from "./session-custom-mcp-card"; import { capabilityConnectPlan, capabilityErrorToast, connectionHealth, } from "../../lib/capabilities"; import { userErrorText } from "../../../ui/api-error"; import { hasWorkspacePermission } from "../../lib/permissions"; import type { CapabilityCatalogItem, ResourceRef } from "../../types"; import type { ChatSendContext } from "./session-github-repositories"; import { SessionGitHubCapabilityCard } from "./session-github-card"; import { RepeatedMachineRequestNote, SessionMachineCapabilityCard } from "./session-machine-card"; import { SessionChromeCapabilityCard } from "./session-chrome-card"; import { usePage } from "../../lib/navigation"; /** The built-in card for connecting a person's own computer to this chat. */ export const CONNECTED_MACHINE_CAPABILITY_ID = "api:connected-machine"; /** The built-in tile and card that install the deployment's Excel add-in. */ export const EXCEL_ADDIN_CAPABILITY_ID = "api:excel-addin"; /** Recommendations carry identity and rationale, never connection configuration. * Expand against the current authenticated catalog before rendering any form. */ type SessionCapabilityCardProps = { visibility?: "private" | "workspace" | undefined; onConfigured?: (() => Promise) | undefined; item: AuthNeededItem; workspaceId: string; sessionId: string; /** The chat's mounted resources, for cards that add a resource to this chat. */ resources?: readonly ResourceRef[] | undefined; /** What a composer Send would carry now, for cards that send a human message. */ sendContext?: (() => ChatSendContext) | undefined; }; export function SessionCapabilityCard(props: SessionCapabilityCardProps) { const page = usePage(); const context = useAppContext(); const [registered, setRegistered] = useState<{ id: string; restoreFocus: boolean } | null>(null); const onRegistered = useCallback( (id: string, restoreFocus: boolean) => setRegistered({ id, restoreFocus }), [], ); if (props.item.setupRequest && !registered) { return ( ); } const item = registered ? { ...props.item, setupRequest: null, capability: { id: registered.id, name: props.item.setupRequest!.name, kind: "mcp" as const, source: "manual" as const, action: "connect" as const, rationale: props.item.setupRequest!.rationale, requiredVariables: [], }, } : props.item; const capability = item.capability; if (capability?.id === CONNECTED_MACHINE_CAPABILITY_ID) { // The same request was made again further down, where its card is live. if (item.askedAgainLater) return ; if (item.purpose === "chrome") { return ( ); } return ( ); } if (capability?.id === "api:github-app") { return ( ); } const resolved = context.workspaceCapabilityCatalog.find((entry) => entry.id === capability?.id); if (capability && resolved?.kind === "mcp" && resolved.authKind === "oauth2") { return ( ); } return ( ); } function ScopedSessionCapabilityCard({ item, workspaceId, sessionId, onConfigured, restoreFocus = false, }: SessionCapabilityCardProps & { restoreFocus?: boolean | undefined }) { const context = useAppContext(); const recommendation = item.capability!; const [expanded, setExpanded] = useState(false); const [complete, setComplete] = useState(false); const [busy, setBusy] = useState(false); const [resolvedItem, setResolvedItem] = useState(null); const opener = useRef(null); const cardRef = useRef(null); useEffect(() => { if (!restoreFocus) return; // The review dialog's opener is removed when the new connection card // replaces it. Focus the next actionable control after Radix closes it. const frame = requestAnimationFrame(() => opener.current?.focus()); return () => cancelAnimationFrame(frame); }, [restoreFocus]); const catalogItem = resolvedItem ?? context.workspaceCapabilityCatalog.find((entry) => entry.id === recommendation.id); const logo = catalogItem ? capabilityLogoSource(catalogItem, (path) => context.client.catalogAssetUrl(path)) : null; const close = () => { setExpanded(false); }; const skill = recommendation.kind === "skill"; const excel = recommendation.id === EXCEL_ADDIN_CAPABILITY_ID; const apiKey = catalogItem ? capabilityConnectPlan(catalogItem).mode === "api_key" : false; return ( setExpanded(true)} onClose={close} busy={busy} opener={opener} cardRef={cardRef} > {recommendation.id === "mcp:codex_apps" ? ( { setComplete(true); close(); }} /> ) : ( { setComplete(true); close(); }} /> )} ); } function SessionCapabilitySetup({ busy, setBusy, capabilityId, onResolvedItem, onConfigured, workspaceId, sessionId, onClose, onComplete, }: { busy: boolean; setBusy: (busy: boolean) => void; capabilityId: string; onResolvedItem: (item: CapabilityCatalogItem) => void; onConfigured?: (() => Promise) | undefined; workspaceId: string; sessionId: string; onClose: () => void; onComplete: () => void; }) { const page = usePage(); const context = useAppContext(); const catalog = useCapabilitiesCatalog(workspaceId); const canReadConnections = context.accessContext === null ? null : hasWorkspacePermission(context.accessContext, workspaceId, "connections:read"); const [error, setError] = useState(null); const subjectId = context.accessContext?.subjectId ?? null; const [parentAccessReview, setParentAccessReview] = useState<{ plan: SessionCapabilityAccessPlan; invocation: object; } | null>(null); const [authInspection, setAuthInspection] = useState<{ id: string; url: string; kind: "oauth2" | "none" | "unknown"; message?: string | undefined; } | null>(null); const [authInspectionRevision, setAuthInspectionRevision] = useState(0); const inFlight = useRef(null); const scope = useRef({ client: context.client, workspaceId, sessionId, canReadConnections, subjectId, authorityKey: catalog.authorityKey, alive: true, }); if ( scope.current.client !== context.client || scope.current.workspaceId !== workspaceId || scope.current.sessionId !== sessionId || scope.current.canReadConnections !== canReadConnections || scope.current.subjectId !== subjectId || scope.current.authorityKey !== catalog.authorityKey ) { scope.current = { client: context.client, workspaceId, sessionId, canReadConnections, subjectId, authorityKey: catalog.authorityKey, alive: true, }; } const parentAccessPlan = parentAccessReview?.invocation === scope.current ? parentAccessReview.plan : null; useEffect(() => { // StrictMode replays setup after cleanup. Give the replay a new owner: // reusing the retired object would either keep it dead or revive old work. if (!scope.current.alive) scope.current = { ...scope.current, alive: true }; const activeScope = scope.current; setParentAccessReview(null); setBusy(false); void catalog.refresh(); return () => { activeScope.alive = false; }; // Refetch on a live read-grant change; the hook masks prior rows during render. // eslint-disable-next-line react-hooks/exhaustive-deps }, [ context.client, workspaceId, sessionId, canReadConnections, subjectId, catalog.authorityKey, setBusy, ]); const rawItem = catalog.items.find((entry) => entry.id === capabilityId); const rawItemId = rawItem?.id; const inspectUrl = rawItem?.mcpUrl ?? rawItem?.endpointUrl; const needsAuthInspection = rawItem?.kind === "mcp" && !rawItem.enabled && capabilityConnectPlan(rawItem).mode === "setup_required" && Boolean(inspectUrl); useEffect(() => { if (!needsAuthInspection || !rawItemId || !inspectUrl) return; let active = true; setAuthInspection(null); void context.client.inspectMcpAuthentication(workspaceId, inspectUrl).then( (result) => { if (active) setAuthInspection({ id: rawItemId, url: inspectUrl, ...result }); }, () => { if (active) setAuthInspection({ id: rawItemId, url: inspectUrl, kind: "unknown" }); }, ); return () => { active = false; }; }, [ context.client, workspaceId, rawItemId, inspectUrl, needsAuthInspection, authInspectionRevision, ]); const item = useMemo(() => { if (!rawItem || !needsAuthInspection) return rawItem; const inspection = authInspection?.id === rawItem.id && authInspection.url === inspectUrl ? authInspection.kind : "checking"; return { ...rawItem, authKind: inspection === "oauth2" ? ("oauth2" as const) : inspection === "none" ? ("none" as const) : null, metadata: { ...rawItem.metadata, authDiscovery: inspection, authDiscoveryMessage: inspection === "unknown" ? authInspection?.message : undefined, }, }; }, [rawItem, needsAuthInspection, inspectUrl, authInspection]); useEffect(() => { if (item) onResolvedItem(item); }, [item, onResolvedItem]); const refreshRuntime = useCallback(() => { void context.refreshWorkspaceMcpServers(workspaceId); }, [context, workspaceId]); async function act(action: ConnectAction) { if (!item) return; const invocation = scope.current; if (inFlight.current === invocation) return; inFlight.current = invocation; setBusy(true); setError(null); setParentAccessReview(null); const current = () => scope.current === invocation && invocation.alive; try { await performCapabilityAction( { client: context.client, workspaceId, item, connections: catalog.connectionsLoadFailed ? null : catalog.connections, canManageSkills: hasWorkspacePermission( context.accessContext, workspaceId, "capabilities:manage", ), refresh: catalog.refresh, onRuntimeChanged: () => { if (current()) refreshRuntime(); }, onComplete: async () => { if (!current()) return; const updated = (await context.client.listCapabilities(workspaceId)).items.find( (entry) => entry.id === item.id, ); if (!current()) return; if (!updated?.enabled) throw new Error("Setup could not be verified. Refresh and try again."); const plan = await prepareSessionCapabilityAccess( t, context.client, workspaceId, sessionId, updated, current, ); if (!current()) return; if (plan.sessions.some((chat) => chat.id !== sessionId && chat.request)) { setParentAccessReview({ plan, invocation }); return; } await applySessionCapabilityAccess(t, context.client, plan, current); await onConfigured?.(); if (current()) onComplete(); }, onSkillRemoval: () => { throw new Error( "Use the workspace Skill controls to review removal and its other owners.", ); }, returnPathFor: (id) => `/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}?capability_auth=${encodeURIComponent(id)}`, redirect: (url) => { if (current()) page.assign(url); }, }, action, ); } catch (failure) { // A credential may have committed before enabling failed. Reconcile before // offering Retry so it reuses that row instead of minting a duplicate. if (current()) await catalog.refresh(); if (current()) setError(capabilityErrorToast(failure, "Couldn't complete setup").description); } finally { if (inFlight.current === invocation) inFlight.current = null; if (current()) setBusy(false); } } async function useConnected() { if (!item) return; const invocation = scope.current; if (inFlight.current === invocation) return; inFlight.current = invocation; setBusy(true); setError(null); const current = () => scope.current === invocation && invocation.alive; try { const plan = parentAccessPlan ?? (await prepareSessionCapabilityAccess( t, context.client, workspaceId, sessionId, item, current, )); if (!current()) return; if ( !parentAccessPlan && plan.sessions.some((chat) => chat.id !== sessionId && chat.request) ) { setParentAccessReview({ plan, invocation }); return; } await applySessionCapabilityAccess(t, context.client, plan, current); if (current()) setParentAccessReview(null); if (current()) await onConfigured?.(); if (current()) onComplete(); } catch (failure) { if (current()) { setParentAccessReview(null); setError(`Couldn't add this connection. ${userErrorText(failure, "Try again.")}`); } } finally { if (inFlight.current === invocation) inFlight.current = null; if (current()) setBusy(false); } } const ownsActionRow = !catalog.loadError && item && !item.enabled && (item.kind === "skill" || item.kind === "mcp"); const health = item ? connectionHealth(item, catalog.connections ?? [], catalog.connections !== null) : null; return (
{catalog.loading && !item ? (

Loading connection details…

) : catalog.loadError || !item ? ( void catalog.refresh()}> Retry } > {catalog.loadError ? "Couldn't load the integration catalog." : "This recommendation is no longer in the current catalog."} ) : ( context.client.catalogAssetUrl(path))} busy={busy} errorMessage={error} socialConnections={catalog.socialConnections} canManageSocial={hasWorkspacePermission( context.accessContext, workspaceId, "connections:write", )} canManageSkills={hasWorkspacePermission( context.accessContext, workspaceId, "capabilities:manage", )} onAction={(action) => void act(action)} onRetryAuthInspection={ needsAuthInspection ? () => { setAuthInspection(null); setAuthInspectionRevision((revision) => revision + 1); } : undefined } /> )} {parentAccessPlan && item ? (

This chat inherits its tool access from its parent. To add {item.name}, enable its tools in these parent chats first:

Confirming adds {item.name}'s tools to these chats and this chat. Parent chats and future children can then use them. Existing tool choices are preserved; workspace defaults become chat-specific selections where needed. Current turns keep their accepted access.

Updates are saved separately. If a later update fails, approved parent changes may already be saved.

) : null}
{!ownsActionRow ? ( ) : null} {item?.enabled && health?.state !== "attention" && health?.state !== "unverified" ? ( ) : null}
); } function SessionCodexAppsSetup({ busy, setBusy, workspaceId, sessionId, onClose, onComplete, }: { busy: boolean; setBusy: (busy: boolean) => void; workspaceId: string; sessionId: string; onClose: () => void; onComplete: () => void; }) { const context = useAppContext(); const [error, setError] = useState(null); const inFlight = useRef(false); const active = useRef(true); useEffect( () => () => { active.current = false; }, [], ); async function useApps() { if (inFlight.current) return; inFlight.current = true; setBusy(true); setError(null); try { const catalog = await context.client.listCapabilities(workspaceId); const item = catalog.items.find((entry) => entry.id === "mcp:codex_apps"); if (!active.current) return; if (!item?.enabled || !item.runtime.available) throw new Error("Choose an active subscription for Codex Apps before continuing."); await attachSessionCapability( t, context.client, workspaceId, sessionId, item, () => active.current, ); await context.refreshWorkspaceMcpServers(workspaceId); if (active.current) onComplete(); } catch (failure) { if (active.current) setError( `Couldn't enable Apps in this conversation. ${userErrorText(failure, "Try again.")}`, ); } finally { inFlight.current = false; if (active.current) setBusy(false); } } return (
Loading connection controls…

}>
{error ? {error} : null}
); }