/**
* @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
onToggle(adapter.type, !adapter.disabled)}
>
{onReload && (
onReload(adapter.type)}
>
)}
{onReinstall && (
onReinstall(adapter.type)}
>
)}
{canRemove && (
onRemove(adapter.type)}
>
)}
);
}
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 Adapter
Install External Adapter
Add an adapter from npm or a local path. The adapter package must export createServerAdapter().
{/* Source toggle */}
setIsLocalPath(false)}
>
npm package
setIsLocalPath(true)}
>
Local path
{isLocalPath ? (
/* Local path input */
) : (
/* npm package input */
<>
Package Name
setInstallPackage(e.target.value)}
/>
Version (optional)
setInstallVersion(e.target.value)}
/>
>
)}
setInstallDialogOpen(false)}>Cancel
installMutation.mutate({
packageName: installPackage,
version: installVersion || undefined,
isLocalPath,
})
}
disabled={!installPackage || installMutation.isPending}
>
{installMutation.isPending ? "Installing..." : "Install"}
{/* 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.
setRemoveType(null)}>Cancel
{
if (removeType) {
removeMutation.mutate(removeType, {
onSettled: () => setRemoveType(null),
});
}
}}
>
{removeMutation.isPending ? "Removing..." : "Remove"}
);
}