/** * @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"; 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.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 && ( )}
  • ); } 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); 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); 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); 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) => reinstallMutation.mutate(type) : 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.
    ); }