/** * @fileoverview Adapter Manager page — install, view, and manage external adapters. * * Adapters are simpler than plugins: no workers, no events, no manifests. * They just register a ServerAdapterModule that provides model discovery and execution. */ import { useEffect, useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, Cpu, Plus, Power, Trash2, FolderOpen, Package, RefreshCw, Download } from "lucide-react"; import { useCompany } from "@/context/CompanyContext"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { adaptersApi } from "@/api/adapters"; import type { AdapterInfo } from "@/api/adapters"; import { getAdapterLabel } from "@/adapters/adapter-display-registry"; import { queryKeys } from "@/lib/queryKeys"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { useToast } from "@/context/ToastContext"; import { cn } from "@/lib/utils"; import { ChoosePathButton } from "@/components/PathInstructionsModal"; import { invalidateDynamicParser } from "@/adapters/dynamic-loader"; import { invalidateConfigSchemaCache } from "@/adapters/schema-config-fields"; function AdapterRow({ adapter, canRemove, onToggle, onRemove, onReload, onReinstall, isToggling, isReloading, isReinstalling, }: { adapter: AdapterInfo; canRemove: boolean; onToggle: (type: string, disabled: boolean) => void; onRemove: (type: string) => void; onReload?: (type: string) => void; onReinstall?: (type: string) => void; isToggling: boolean; isReloading?: boolean; isReinstalling?: boolean; }) { return (
  • {adapter.label || getAdapterLabel(adapter.type)} {adapter.source === "external" ? "External" : "Built-in"} {adapter.overriddenBuiltin && ( Overrides built-in )} {adapter.source === "external" && ( adapter.isLocalPath ? : )} {adapter.loaded ? "loaded" : "error"} {adapter.version && ( v{adapter.version} )} {adapter.disabled && ( Hidden from menus )}

    {adapter.type} {adapter.packageName && adapter.packageName !== adapter.type && ( <> · {adapter.packageName} )} {" · "}{adapter.modelsCount} models

    {onReload && ( )} {onReinstall && ( )} {canRemove && ( )}
  • ); } function fetchNpmLatestVersion(packageName: string): Promise { return fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { signal: AbortSignal.timeout(5000), }) .then((res) => res.json()) .then((data) => (typeof data?.version === "string" ? (data.version as string) : null)) .catch(() => null); } function ReinstallDialog({ adapter, open, isReinstalling, onConfirm, onCancel, }: { adapter: AdapterInfo | null; open: boolean; isReinstalling: boolean; onConfirm: () => void; onCancel: () => void; }) { const { data: latestVersion, isLoading: isFetchingVersion } = useQuery({ queryKey: ["npm-latest-version", adapter?.packageName], queryFn: () => { if (!adapter?.packageName) return null; return fetchNpmLatestVersion(adapter.packageName); }, enabled: open && !!adapter?.packageName, staleTime: 60_000, }); const isUpToDate = adapter?.version && latestVersion && adapter.version === latestVersion; return ( { if (!o) onCancel(); }}> Reinstall Adapter This will pull the latest version of{" "} {adapter?.packageName} from npm and hot-swap the running adapter module. Existing agents will use the new version on their next run.
    Package {adapter?.packageName}
    Current {adapter?.version ? `v${adapter.version}` : "unknown"}
    Latest on npm {isFetchingVersion ? "checking..." : latestVersion ? `v${latestVersion}` : "unavailable"}
    {isUpToDate && (

    Already on the latest version.

    )}
    ); } export function AdapterManager() { const { selectedCompany } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const { pushToast } = useToast(); const [installPackage, setInstallPackage] = useState(""); const [installVersion, setInstallVersion] = useState(""); const [isLocalPath, setIsLocalPath] = useState(false); const [installDialogOpen, setInstallDialogOpen] = useState(false); const [removeType, setRemoveType] = useState(null); const [reinstallTarget, setReinstallTarget] = useState(null); useEffect(() => { setBreadcrumbs([ { label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: "Settings", href: "/instance/settings/general" }, { label: "Adapters" }, ]); }, [selectedCompany?.name, setBreadcrumbs]); const { data: adapters, isLoading } = useQuery({ queryKey: queryKeys.adapters.all, queryFn: () => adaptersApi.list(), }); const invalidate = () => { queryClient.invalidateQueries({ queryKey: queryKeys.adapters.all }); }; const installMutation = useMutation({ mutationFn: (params: { packageName: string; version?: string; isLocalPath?: boolean }) => adaptersApi.install(params), onSuccess: (result) => { invalidate(); setInstallDialogOpen(false); setInstallPackage(""); setInstallVersion(""); setIsLocalPath(false); pushToast({ title: "Adapter installed", body: `Type "${result.type}" registered successfully.${result.version ? ` (v${result.version})` : ""}`, tone: "success", }); }, onError: (err: Error) => { pushToast({ title: "Install failed", body: err.message, tone: "error" }); }, }); const removeMutation = useMutation({ mutationFn: (type: string) => adaptersApi.remove(type), onSuccess: () => { invalidate(); pushToast({ title: "Adapter removed", tone: "success" }); }, onError: (err: Error) => { pushToast({ title: "Removal failed", body: err.message, tone: "error" }); }, }); const toggleMutation = useMutation({ mutationFn: ({ type, disabled }: { type: string; disabled: boolean }) => adaptersApi.setDisabled(type, disabled), onSuccess: () => { invalidate(); }, onError: (err: Error) => { pushToast({ title: "Toggle failed", body: err.message, tone: "error" }); }, }); const reloadMutation = useMutation({ mutationFn: (type: string) => adaptersApi.reload(type), onSuccess: (result) => { invalidate(); invalidateDynamicParser(result.type); invalidateConfigSchemaCache(result.type); pushToast({ title: "Adapter reloaded", body: `Type "${result.type}" reloaded.${result.version ? ` (v${result.version})` : ""}`, tone: "success", }); }, onError: (err: Error) => { pushToast({ title: "Reload failed", body: err.message, tone: "error" }); }, }); const reinstallMutation = useMutation({ mutationFn: (type: string) => adaptersApi.reinstall(type), onSuccess: (result) => { invalidate(); invalidateDynamicParser(result.type); invalidateConfigSchemaCache(result.type); pushToast({ title: "Adapter reinstalled", body: `Type "${result.type}" updated from npm.${result.version ? ` (v${result.version})` : ""}`, tone: "success", }); }, onError: (err: Error) => { pushToast({ title: "Reinstall failed", body: err.message, tone: "error" }); }, }); const builtinAdapters = (adapters ?? []).filter((a) => a.source === "builtin"); const externalAdapters = (adapters ?? []).filter((a) => a.source === "external"); if (isLoading) return
    Loading adapters...
    ; const isMutating = installMutation.isPending || removeMutation.isPending || toggleMutation.isPending || reloadMutation.isPending || reinstallMutation.isPending; return (
    {/* Header */}

    Adapters

    Alpha
    Install External Adapter Add an adapter from npm or a local path. The adapter package must export createServerAdapter().
    {/* Source toggle */}
    {isLocalPath ? ( /* Local path input */
    setInstallPackage(e.target.value)} />

    Accepts Linux, WSL, and Windows paths. Windows paths are auto-converted.

    ) : ( /* npm package input */ <>
    setInstallPackage(e.target.value)} />
    setInstallVersion(e.target.value)} />
    )}
    {/* Alpha notice */}

    External adapters are alpha.

    The adapter plugin system is under active development. APIs and storage format may change. Use the power icon to hide adapters from agent menus without removing them.

    {/* External adapters */}

    External Adapters

    {externalAdapters.length === 0 ? (

    No external adapters installed

    Install an adapter package to extend model support.

    ) : (
      {externalAdapters.map((adapter) => ( toggleMutation.mutate({ type, disabled })} onRemove={(type) => setRemoveType(type)} onReload={(type) => reloadMutation.mutate(type)} onReinstall={!adapter.isLocalPath ? (type) => setReinstallTarget(adapter) : undefined} isToggling={toggleMutation.isPending} isReloading={reloadMutation.isPending} isReinstalling={reinstallMutation.isPending} /> ))}
    )}
    {/* Built-in adapters */}

    Built-in Adapters

    {builtinAdapters.length === 0 ? (
    No built-in adapters found.
    ) : (
      {builtinAdapters.map((adapter) => ( toggleMutation.mutate({ type, disabled })} onRemove={() => {}} isToggling={isMutating} /> ))}
    )}
    {/* Remove confirmation */} { if (!open) setRemoveType(null); }} > Remove Adapter Are you sure you want to remove the {removeType} adapter? It will be unregistered and removed from the adapter store. {removeType && adapters?.find((a) => a.type === removeType)?.packageName && ( <> npm packages will be cleaned up from disk. )} {" "}This action cannot be undone. {/* Reinstall confirmation */} { if (reinstallTarget) { reinstallMutation.mutate(reinstallTarget.type, { onSettled: () => setReinstallTarget(null), }); } }} onCancel={() => setReinstallTarget(null)} />
    ); }