import { OwnershipSelector } from "./connection-ownership-selector"; export { OwnershipSelector } from "./connection-ownership-selector"; import { ExternalLinkIcon, Loader2Icon, PlugIcon, RefreshCwIcon, SparklesIcon, TrashIcon, } from "lucide-react"; import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type ReactNode, type RefObject, } from "react"; import { ConnectorToolPermissions, hasConnectorToolPermissionTarget, } from "./connector-tool-permissions"; import { CapabilityLogo } from "./capability-logo"; import { McpAuthDiscoveryNotice } from "./mcp-auth-discovery-notice"; import { OnePasswordCatalogLink } from "./onepassword-catalog-link"; import { ExcelAddinSetupSteps } from "./excel-addin-catalog-link"; import { CapabilityDialogContent } from "./detail-dialog"; import { capabilityPresentation, presentationPermissions, type IntegrationPresentationCopy, } from "./integration-experience"; import { Button } from "../../../ui/button"; import { Input } from "../../../ui/input"; import { Label } from "../../../ui/label"; import { MetaChip } from "../../../ui/meta-chip"; import { Notice } from "../../../ui/notice"; import { Dialog as Sheet, DialogDescription as SheetDescription, DialogHeader as SheetHeader, DialogTitle as SheetTitle, } from "../../../ui/dialog"; import { capabilityCategoryLabel, capabilityConnectPlan, capabilityItemKindLabel, capabilityReconnectPlan, defaultCapabilityConnectionOwnership, capabilitySourceLabel, curatedSkillProvenance, socialConnectionsForOwnership, DEFAULT_CONNECTION_OWNERSHIP, GENERIC_API_KEY_FIELD, type ConnectionHealth, } from "../../lib/capabilities"; import { focusCapabilitySuccessor } from "../../lib/capability-focus"; import { analyticsAction, type AnalyticsAction } from "../../../ui/analytics-actions"; import { cn } from "../../lib/utils"; import type { CapabilityCatalogItem, ConnectionMetadata, ConnectionOwnership, SocialConnection, } from "../../types"; // Imported where its classes are used: a module that only re-exported // would be dropped with its stylesheet by tree shaking. import "./session-capability-card.css"; export type ConnectAction = | { type: "enable"; item: CapabilityCatalogItem } | { type: "install_skill"; item: CapabilityCatalogItem } | { type: "remove_skill"; item: CapabilityCatalogItem } | { type: "social_oauth"; item: CapabilityCatalogItem; provider: "x" | "reddit"; ownership: ConnectionOwnership; } | { type: "disconnect_social"; item: CapabilityCatalogItem; connectionId: string; } // connectionId is the existing Fiken row to rewrite in place (reconnect / // replace token), or null for a first connect. | { type: "fiken_api_token"; item: CapabilityCatalogItem; apiToken: string; connectionId: string | null; } | { type: "fiken_disconnect"; item: CapabilityCatalogItem; connectionId: string; } // OAuth against the registered Fiken app; connectionId re-authorizes an // existing row in place. | { type: "fiken_oauth"; item: CapabilityCatalogItem; connectionId: string | null; } | { type: "oauth"; item: CapabilityCatalogItem; ownership: ConnectionOwnership; } | { type: "api_key"; item: CapabilityCatalogItem; ownership: ConnectionOwnership; headers: Record; } // connectionId is the existing row to reuse, or null when the row was deleted // (reconnect then mints a fresh connection and re-enables with its ref). | { type: "reconnect_oauth"; item: CapabilityCatalogItem; connectionId: string | null; ownership: ConnectionOwnership; /** The account's own workspace when it was connected in another one. */ connectionWorkspaceId?: string | undefined; } // Sign in a further account; never refreshes or replaces an existing one. | { type: "add_oauth_account"; item: CapabilityCatalogItem; ownership: ConnectionOwnership; } // Disconnect exactly one account; the connector itself stays as it is. | { type: "remove_connection"; item: CapabilityCatalogItem; connection: Pick; } | { type: "reconnect_api_key"; item: CapabilityCatalogItem; connectionId: string | null; ownership: ConnectionOwnership; headers: Record; } | { type: "disconnect"; item: CapabilityCatalogItem }; // Re-exported from the shared library so the sheet and the row/tile quick-connect // fast path can never drift on what a new connection defaults to. export { DEFAULT_CONNECTION_OWNERSHIP }; export function CapabilityDetailSheet({ workspaceId, item, health, logoSrc, open, onOpenChange, restoreFocusRef, restoreFocusFallbackRef, busy, errorMessage, socialConnections, canManageSocial = false, canManageSkills = false, onAction, }: { workspaceId?: string | undefined; item: CapabilityCatalogItem | null; health: ConnectionHealth; logoSrc: string | null; open: boolean; onOpenChange: (open: boolean) => void; restoreFocusRef?: RefObject | undefined; restoreFocusFallbackRef?: RefObject | undefined; busy: boolean; errorMessage: string | null; socialConnections?: SocialConnection[] | undefined; canManageSocial?: boolean | undefined; canManageSkills?: boolean | undefined; onAction: (action: ConnectAction) => void; }) { const localRestoreFocusRef = useRef(null); const focusRef = restoreFocusRef ?? localRestoreFocusRef; const focusTargetIdRef = useRef(null); // The selected item is cleared at the same time the controlled sheet closes. // Retain its identity independently so the close autofocus hook can find the // newly rendered Enabled control after a successful enable refresh. useLayoutEffect(() => { if (item) focusTargetIdRef.current = item.id; }, [item]); // Capture before Radix's focus scope moves focus into the sheet. Routes pass // a synchronously captured opener for click/keyboard activation; this local // fallback keeps the controlled sheet safe for other callers too. useLayoutEffect(() => { if (!open) return; if (focusRef.current) return; const active = document.activeElement; if (active instanceof HTMLElement && active !== document.body) { focusRef.current = active; } }, [focusRef, open]); return ( { const opener = focusRef.current; focusRef.current = null; if (opener?.isConnected) { event.preventDefault(); opener.focus(); return; } const restore = () => focusCapabilitySuccessor( focusTargetIdRef.current, restoreFocusFallbackRef?.current ?? null, ); event.preventDefault(); // Refresh + close normally commit the Enabled control before Radix // invokes this hook. One frame covers the rare slower commit without // allowing Radix to restore focus to document.body in the meantime. if (!restore()) { if (typeof window.requestAnimationFrame === "function") { window.requestAnimationFrame(restore); } else { window.setTimeout(restore, 0); } } }} > {item ? ( ) : null} ); } export function DetailBody({ workspaceId, item, inline = false, setupOnly = inline, showIdentity = true, onCancel, health, logoSrc, busy, errorMessage, socialConnections, canManageSocial, canManageSkills = false, onAction, onRetryAuthInspection, }: { workspaceId?: string | undefined; item: CapabilityCatalogItem; inline?: boolean | undefined; /** Chat setup must not expose disconnect/removal actions, regardless of layout. */ setupOnly?: boolean | undefined; /** The containing dialog may already render the provider identity. */ showIdentity?: boolean | undefined; onCancel?: (() => void) | undefined; health: ConnectionHealth; logoSrc: string | null; busy: boolean; errorMessage: string | null; socialConnections?: SocialConnection[] | undefined; canManageSocial: boolean; canManageSkills?: boolean | undefined; onAction: (action: ConnectAction) => void; onRetryAuthInspection?: (() => void) | undefined; }) { const plan = useMemo(() => capabilityConnectPlan(item), [item]); const defaultOwnership = defaultCapabilityConnectionOwnership(item); // API-key reconnect reveals the credential form in place of the button. const [reconnecting, setReconnecting] = useState(false); useEffect(() => setReconnecting(false), [item.id]); const [connectionOwnership, setConnectionOwnership] = useState(defaultOwnership); useEffect(() => setConnectionOwnership(defaultOwnership), [item.id, defaultOwnership]); const canDisconnect = !setupOnly && item.enabled && item.kind === "mcp" && item.actions.includes("disconnect"); const keyPageUrl = item.installUrl ?? item.homepageUrl; // Repair is driven by the installation's OWN connectionRef.kind, not the catalog // plan — on catalog/registry drift an enabled item can carry a live connectionRef // while its current catalog auth fields read as plain "enable", and gating on the // plan would leave "Needs attention" with no Reconnect (the dead end we killed). const reconnect = capabilityReconnectPlan(item, health); // When the catalog no longer supplies requiredHeaders, fall back to one generic // "API key" field so an api-key reconnect still has something to submit. const reconnectFields = plan.mode === "api_key" && plan.fields.length > 0 ? plan.fields : [GENERIC_API_KEY_FIELD]; const SkillHeading = inline ? "h4" : "h3"; return (
{!showIdentity ? null : inline ? (

{item.name}

{item.providerDomain ? (

{item.providerDomain}

) : null}
{capabilityItemKindLabel(item)}
) : (
: undefined} />
{item.name} {capabilityItemKindLabel(item)} {capabilityCategoryLabel(item.category) ? ` · ${capabilityCategoryLabel(item.category)}` : ""}
)} {/* Scrollable body */}
{item.stale ? ( No longer listed in the public registry. Existing installations keep working. ) : null} {item.description && (!inline || showIdentity) && !(setupOnly && item.kind === "skill") ? (

{item.description}

) : null} {!inline && item.tags.length > 0 ? (
{item.tags.slice(0, 10).map((tag) => ( {tag} ))}
) : null} {!inline ? (
{capabilitySourceLabel(item.source)} {item.homepageUrl ? ( ) : null} {item.installUrl && item.installUrl !== item.homepageUrl ? ( ) : null} {item.endpointUrl ? ( {item.endpointUrl} ) : null}
) : null} {setupOnly && item.kind === "skill" ? (
What this skill adds

{item.description || "Review this skill's source and version before installing."}

{curatedSkillProvenance(item) ? (
Review source and version
) : null}
) : ( )} {workspaceId && !setupOnly && hasConnectorToolPermissionTarget(item, health) ? ( ) : null} {/* Action — flows directly after the content so a sparse item stays a compact top-flowing column, with no dead void before a bottom-pinned button. The whole body scrolls only when content actually overflows. */}
button]:ml-auto [&>button]:flex [&>button]:w-auto [&>div>button]:ml-auto [&>div>button]:flex [&>div>button]:w-auto" : "border-t border-border pt-5", )} > {errorMessage ? {errorMessage} : null} {item.surfaceType === "codex_apps" ? (
{item.runtime.available ? "Available through the active workspace Apps designation." : "Unavailable. An active Codex Apps credential must be designated in Workspace Settings before it can be selected."}

{item.runtime.available ? "Select Codex Apps from a session's Tools picker, or leave the policy at the workspace default." : "This surface cannot be enabled from the capability catalog; its authorization is managed by the workspace Codex subscription."}

) : item.surfaceType === "first_party_excel_addin" ? ( ) : item.surfaceType === "first_party_onepassword" && workspaceId ? (

{setupOnly ? "Opens Secrets in a new tab. " : ""} Connect a service account and choose which vaults agents can use.

) : item.kind === "skill" ? ( ) : plan.mode === "social_oauth" ? ( ) : plan.mode === "fiken_api_token" ? ( ) : item.enabled ? (
{/* Reconnect is the primary repair action when the connection broke; Disable drops to secondary. Healthy items show only Disable. */} {reconnect ? ( reconnect.kind === "oauth" ? ( ) : reconnecting ? ( } busy={busy} onSubmit={(next) => onAction({ type: "reconnect_api_key", item, connectionId: reconnect.connectionId, ownership: reconnect.ownership, headers: next, }) } /> ) : ( ) ) : null} {canDisconnect ? ( ) : !setupOnly ? (

Manage this capability from its dedicated controls.

) : null}
) : plan.mode === "api_key" ? (

You’ll use credentials for your own provider account or service account.

} busy={busy} onSubmit={(next) => onAction({ type: "api_key", item, ownership: connectionOwnership, headers: next, }) } />
) : plan.mode === "oauth" ? (

You’ll sign in with your own account.

) : plan.mode === "setup_required" ? ( ) : item.kind === "mcp" ? ( ) : (

Install or connect this capability from its dedicated controls.

)}
); } function SkillControls({ item, busy, canManage, onAction, onCancel, setupOnly = false, }: { item: CapabilityCatalogItem; busy: boolean; onCancel?: (() => void) | undefined; setupOnly?: boolean | undefined; canManage: boolean; onAction: (action: ConnectAction) => void; }) { const updateAvailable = item.metadata.updateAvailable === true; return (
{setupOnly && !item.enabled ? (

Install for

Workspace

Available to everyone on the team in this workspace.

This library skill can currently be installed for the workspace only.

) : null} {item.enabled ? (
{updateAvailable ? "Installed · update available" : "Installed"}
) : null} {!item.enabled || updateAvailable ? ( ) : null} {item.enabled && !setupOnly ? ( ) : null} {!canManage ? (

Workspace administrator permission is required to install, update, or remove Skills.

) : null}

Skills add reviewed instructions only. They never grant credentials or connect provider accounts.

); } /** * Curated consent copy for a connector's OAuth connect: what agents will do * and what the requested scopes mean in plain language. Rendered only when the * catalog row carries `metadata.presentation`; an uncurated connector keeps * the plain connect button exactly as before. Presentation-only - the actual * grant is whatever the provider's consent screen shows. */ function ConnectorConsentCopy({ presentation, requestedScopes, }: { presentation: IntegrationPresentationCopy | undefined; requestedScopes: string[]; }) { if (!presentation) return null; const permissions = presentationPermissions(requestedScopes, presentation); // Content-derived keys with an ordinal suffix only for exact duplicates, so // reordering or removing unrelated entries never remounts a row. const uniqueKeys = (contents: string[]): string[] => { const seen = new Map(); return contents.map((content) => { const occurrence = seen.get(content) ?? 0; seen.set(content, occurrence + 1); return occurrence === 0 ? content : `${content}#${occurrence}`; }); }; const capabilityKeys = uniqueKeys( (presentation.capabilities ?? []).map((entry) => `${entry.title}:${entry.description}`), ); const permissionKeys = uniqueKeys( permissions.map((entry) => `${entry.label}:${entry.description}`), ); return (
{presentation.introduction ? (

{presentation.introduction}

) : null} {presentation.capabilities?.length ? (
    {presentation.capabilities.map((capability, index) => (
  • {capability.title}{" "} {capability.description}
  • ))}
) : null} {permissions.length > 0 ? (
    {permissions.map((permission, index) => (
  • {permission.label}{" "} {permission.description}
  • ))}
) : null} {presentation.permissionSummary ? (

{presentation.permissionSummary}

) : null}
); } export function SocialConnectorControls({ item, provider, connections, ownership, onOwnershipChange, busy, canManage, onAction, setupOnly = false, }: { item: CapabilityCatalogItem; provider: "x" | "reddit"; connections: SocialConnection[]; ownership: ConnectionOwnership; onOwnershipChange: (ownership: ConnectionOwnership) => void; busy: boolean; setupOnly?: boolean | undefined; canManage: boolean; onAction: (action: ConnectAction) => void; }) { const visibleConnections = socialConnectionsForOwnership( connections.filter((candidate) => candidate.provider === provider), ownership, ); const connected = visibleConnections.filter((connection) => connection.status === "connected"); const needsReauth = visibleConnections.filter( (connection) => connection.status === "needs_reauth", ); const hasUsableAccount = connected.length + needsReauth.length > 0; const canConnect = ownership === "personal" || canManage; return (

You’ll sign in with your own account.

{visibleConnections.length > 0 ? (
{visibleConnections.map((connection) => (

{connection.accountName || `@${connection.accountHandle}`}

@{connection.accountHandle} · {socialConnectionStatusLabel(connection.status)}

{!setupOnly && connection.status !== "disabled" ? ( ) : null}
))}
) : null} {ownership === "workspace" && !canManage ? (

Workspace admin permission is required to manage this connection.

) : null}
); } const FIKEN_TOKEN_FIELD = { name: "apiToken", label: "Personal API token" }; /** * Connect / reconnect / disconnect controls for the first-party Fiken * connector. Phase 1 is workspace-owned only, so there is no ownership * selector; the token is verified against Fiken before it is stored. */ export function FikenConnectorControls({ item, health, keyPageUrl, busy, onAction, setupOnly = false, oauthAvailable = true, tokenAvailable = true, }: { item: CapabilityCatalogItem; health: ConnectionHealth; keyPageUrl: string | null; busy: boolean; setupOnly?: boolean | undefined; oauthAvailable?: boolean | undefined; tokenAvailable?: boolean | undefined; onAction: (action: ConnectAction) => void; }) { const [replacing, setReplacing] = useState(false); const [usingToken, setUsingToken] = useState(false); useEffect(() => { setReplacing(false); setUsingToken(false); }, [item.id]); const connection = health.state === "connected" || health.state === "attention" ? health.connection : null; const tokenForm = (submitLabel: string, submitIcon: ReactNode) => ( onAction({ type: "fiken_api_token", item, apiToken: next[FIKEN_TOKEN_FIELD.name] ?? "", connectionId: connection?.id ?? null, }) } /> ); const oauthButton = (label: string, icon: ReactNode) => ( ); const tokenFallbackToggle = ( ); if (health.state === "connected" && connection) { return (
{replacing ? (
{oauthButton("Re-authorize with Fiken", )} {tokenForm("Replace API token", )}
) : ( )} {!setupOnly ? ( ) : null}

Credentials are stored encrypted and used only for this workspace's Fiken tools.

); } if (health.state === "attention") { return (
{oauthButton(`Reconnect ${item.name}`, )} {usingToken ? tokenForm("Reconnect with API token", ) : tokenFallbackToggle}
); } return (
{oauthAvailable ? oauthButton("Connect Fiken", ) : null}

Uses your Fiken account. Agents and automations in this workspace can use it.

{tokenAvailable && (usingToken || !oauthAvailable) ? (
{tokenForm("Connect for workspace", )}

Create a personal API token in Fiken under Rediger konto → API. Fiken's terms allow personal tokens only for integrating your own company.

) : tokenAvailable ? ( tokenFallbackToggle ) : null}
); } function socialConnectionStatusLabel(status: SocialConnection["status"]): string { if (status === "connected") return "Connected"; if (status === "needs_reauth") return "Needs reconnection"; return "Disconnected"; } function CuratedSkillProvenanceSection({ item }: { item: CapabilityCatalogItem }) { const metadata = curatedSkillProvenance(item); if (!metadata) return null; return (

Curated skill provenance

Immutable reviewed metadata for the exact artifact selected by this workspace.

{metadata.status === "enabled" ? "Enabled" : "Not enabled"} {humanizeSelection(metadata.effectiveSelection)} {metadata.version ?? "Unavailable"} {metadata.contentSha256 ?? "Unavailable"} {metadata.sourceCommit ?? "Unavailable"} {metadata.provenance ?? "Unavailable"} {metadata.sourceUrl ? ( ) : null} {metadata.documentationUrl ? ( ) : null} {metadata.license ? {metadata.license} : null}
); } function humanizeSelection(value: string): string { const normalized = value.replaceAll("_", " ").trim(); return normalized ? normalized.charAt(0).toUpperCase() + normalized.slice(1) : "Unknown"; } // The labeled credential form, shared by first-time connect and reconnect. It // owns its own header state so it starts empty each time it mounts (a fresh // sheet, or the reveal on reconnect) — credentials are never prefilled. export function CredentialForm({ fields, itemName, keyPageUrl, submitLabel, submitIcon, busy, onSubmit, onCancel, compact = false, analytics, }: { fields: { name: string; label: string }[]; itemName: string; keyPageUrl: string | null; submitLabel: string; submitIcon: ReactNode; busy: boolean; onCancel?: (() => void) | undefined; compact?: boolean | undefined; /** Closed analytics label for the submit control; omit for reconnects. */ analytics?: AnalyticsAction | undefined; onSubmit: (headers: Record) => void; }) { const inputId = useId(); const [headers, setHeaders] = useState>({}); const ready = fields.every((field) => headers[field.name]?.trim()); return (
{ event.preventDefault(); if (ready && !busy) onSubmit(headers); }} > {fields.map((field) => (
setHeaders((current) => ({ ...current, [field.name]: event.target.value, })) } placeholder={`Paste your ${field.label}`} />
))} {keyPageUrl ? ( {compact ? `Where do I find my ${fields[0]?.label ?? "credentials"}?` : `Get your ${fields[0]?.label ?? "credentials"}`} ) : !compact ? (

Stored encrypted and used only to reach {itemName}.

) : null} {compact ? (

Your credential is stored encrypted, never in the conversation.

) : null}
); } export function ConnectionStatus({ item, health, }: { item: CapabilityCatalogItem; health: ConnectionHealth; }) { // "none" = enabled without a connection (headers-enabled or credential-free); // "unverified" = it has a connection but the connections list didn't load, so we // can't check it. Both render a neutral "Enabled" — honest, and never a false // "Needs attention". if (health.state === "none" || health.state === "unverified") { return (
Enabled
); } const attention = health.state === "attention"; const personal = item.connectionRef?.subjectScope === "subject" || (health.connection ? health.connection.subjectId !== null : false); return (
{attention ? "Needs attention" : "Connected"}

{attention ? `${personal ? "Personal" : "Workspace"} connection needs to be reconnected.` : personal ? `Personal connection to ${health.connection.providerDomain}. Your messages and personal schedules can use this account.` : `Workspace connection to ${health.connection.providerDomain}. Shared with agents and automations here.`}

); } function MetaRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function ExternalMetaLink({ href }: { href: string }) { let label = href; try { label = new URL(href).hostname; } catch { // Non-URL string: show it verbatim. } return ( {label} ); } /** The same action row for inline OAuth review, credentials, and Skills. */ function ConnectionActions({ onCancel, busy, children, }: { onCancel?: (() => void) | undefined; busy: boolean; children: ReactNode; }) { if (!onCancel) return children; return (
{children}
); }