import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { CheckCircle, SignIn } from "@phosphor-icons/react"; import type { ConnectorRecord } from "@shared/types"; import { unwrap } from "../lib/desktop"; import { ActionButton, InlineNotice, Input, Select, StatusPill, TextArea } from "./ui"; import { useT } from "../i18n"; function getAuthActionCopy(connector: ConnectorRecord) { switch (connector.id) { case "codex_local": return { title: "Codex login required", message: "Codex is installed, but the CLI needs an authenticated session before AgentCompany can dispatch runs.", buttonLabel: "Open Terminal To Sign In", }; case "claude_local": return { title: "Claude Code login required", message: "Claude Code is installed, but the local CLI is not authenticated for this desktop runtime.", buttonLabel: "Open Terminal To Sign In", }; case "gemini_local": return { title: "Gemini authentication required", message: "Gemini CLI is available, but the runtime still needs an authenticated session. AgentCompany will open Gemini in a terminal and you can complete its interactive auth flow there.", buttonLabel: "Open Terminal", }; } } export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRecord; onSaved: () => Promise }) { const t = useT(); const [model, setModel] = useState(connector.model ?? ""); const [envBindingText, setEnvBindingText] = useState(connector.envBindingText); const [notes, setNotes] = useState(connector.notes); const [submitError, setSubmitError] = useState(null); const [saving, setSaving] = useState(false); const [openingAuthTerminal, setOpeningAuthTerminal] = useState(false); const [autoChecking, setAutoChecking] = useState(false); const [modelOptions, setModelOptions] = useState>([]); const [loadingModelOptions, setLoadingModelOptions] = useState(false); const [modelOptionsError, setModelOptionsError] = useState(null); const authPollTimeoutRef = useRef | null>(null); const authActionCopy = getAuthActionCopy(connector); const needsLogin = connector.authState === "required" || connector.status === "auth_required"; const requiresExplicitModel = false; const shouldLoadModelOptions = connector.capabilityMatrix.modelDiscovery; useEffect(() => { setModel(connector.model ?? ""); setEnvBindingText(connector.envBindingText); setNotes(connector.notes); setSubmitError(null); }, [connector]); useEffect(() => () => { if (authPollTimeoutRef.current) { clearTimeout(authPollTimeoutRef.current); authPollTimeoutRef.current = null; } }, []); useEffect(() => { let cancelled = false; if (!shouldLoadModelOptions) { setModelOptions([]); setModelOptionsError(null); setLoadingModelOptions(false); return; } setLoadingModelOptions(true); setModelOptionsError(null); void window.agentCompany.listConnectorModels(connector.id).then((result) => { if (cancelled) return; if (!result.ok) { setModelOptions([]); setModelOptionsError(result.error.message); setLoadingModelOptions(false); return; } setModelOptions(result.data); setLoadingModelOptions(false); }); return () => { cancelled = true; }; }, [connector.id, shouldLoadModelOptions]); const modelSelectOptions = useMemo(() => { const options = [{ value: "", label: requiresExplicitModel ? "Please select a model..." : "Use connector default" }]; const hasCurrentModel = modelOptions.some((option) => option.id === model); if (model && !hasCurrentModel) { options.push({ value: model, label: `${model} (saved)` }); } options.push(...modelOptions.map((option) => ({ value: option.id, label: option.label || option.id }))); return options; }, [model, modelOptions, requiresExplicitModel]); const runHealthCheck = useCallback(async (quiet = false) => { setAutoChecking(true); try { unwrap(await window.agentCompany.testConnector(connector.id)); await onSaved(); if (!quiet) { setSubmitError(null); } return true; } catch (error) { if (!quiet) { setSubmitError(error instanceof Error ? error.message : "Automatic connector health check failed."); } return false; } finally { setAutoChecking(false); } }, [connector.id, onSaved]); const scheduleAuthPolling = useCallback((remainingAttempts: number) => { if (authPollTimeoutRef.current) { clearTimeout(authPollTimeoutRef.current); authPollTimeoutRef.current = null; } if (remainingAttempts <= 0) { return; } authPollTimeoutRef.current = setTimeout(() => { void (async () => { const ok = await runHealthCheck(true); if (!ok) { scheduleAuthPolling(remainingAttempts - 1); } })(); }, 5000); }, [runHealthCheck]); return (
{submitError ? : null} {needsLogin ? (
{authActionCopy.title}
{authActionCopy.message}
void (async () => { setOpeningAuthTerminal(true); setSubmitError(null); try { unwrap(await window.agentCompany.openConnectorAuthTerminal(connector.id)); scheduleAuthPolling(12); } catch (error) { setSubmitError(error instanceof Error ? error.message : "Could not open a terminal for connector login."); } finally { setOpeningAuthTerminal(false); } })()} loading={openingAuthTerminal} />
) : null}
{t("conn.status")}
{connector.version ?? "version unknown"}
{t("conn.capabilityMatrix")}
{Object.entries(connector.capabilityMatrix).map(([key, value]) => (
{key}
))}
undefined} readOnly />
AgentCompany pins connector execution to the audited binary for this runtime. Command overrides are not accepted from the renderer.
{shouldLoadModelOptions ? (
)}