"use client"; import { useCallback, useEffect, useState, type ChangeEvent, type ReactElement, type SyntheticEvent, } from "react"; import { Pencil, Trash2 } from "lucide-react"; import { FleetSettingsNav } from "@/components/settings/FleetSettingsNav"; import { createAgentProvider, deleteAgentProvider, fetchAgentProviders, updateAgentProvider, type AgentProviderConfig, type CreateAgentProviderRequest, type UpdateAgentProviderRequest, } from "@/lib/api/agent-providers"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; interface ProviderFormData { name: string; provider: "openclaw"; gatewayUrl: string; apiToken: string; isActive: boolean; } const NAME_PATTERN = /^[a-zA-Z0-9-]+$/; const INITIAL_FORM: ProviderFormData = { name: "", provider: "openclaw", gatewayUrl: "", apiToken: "", isActive: true, }; function getErrorMessage(error: unknown, fallback: string): string { if (error instanceof Error && error.message.trim().length > 0) { return error.message; } return fallback; } function isValidHttpsUrl(value: string): boolean { try { const parsed = new URL(value); return parsed.protocol === "https:"; } catch { return false; } } function formatCreatedDate(value: string): string { const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return "Unknown"; } return new Intl.DateTimeFormat(undefined, { year: "numeric", month: "short", day: "numeric", }).format(parsed); } function validateForm(form: ProviderFormData, isEditing: boolean): string | null { const name = form.name.trim(); if (name.length === 0) { return "Name is required."; } if (!NAME_PATTERN.test(name)) { return "Name must contain only letters, numbers, and hyphens."; } const gatewayUrl = form.gatewayUrl.trim(); if (gatewayUrl.length === 0) { return "Gateway URL is required."; } if (!isValidHttpsUrl(gatewayUrl)) { return "Gateway URL must be a valid https:// URL."; } if (!isEditing && form.apiToken.trim().length === 0) { return "API token is required when creating a provider."; } return null; } export default function AgentProvidersSettingsPage(): ReactElement { const [providers, setProviders] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingProvider, setEditingProvider] = useState(null); const [form, setForm] = useState(INITIAL_FORM); const [formError, setFormError] = useState(null); const [isSaving, setIsSaving] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const loadProviders = useCallback(async (showLoadingState: boolean): Promise => { if (showLoadingState) { setIsLoading(true); } else { setIsRefreshing(true); } try { const data = await fetchAgentProviders(); setProviders(data); setError(null); } catch (loadError: unknown) { setError(getErrorMessage(loadError, "Failed to load agent providers.")); } finally { setIsLoading(false); setIsRefreshing(false); } }, []); useEffect(() => { void loadProviders(true); }, [loadProviders]); function openCreateDialog(): void { setEditingProvider(null); setForm(INITIAL_FORM); setFormError(null); setIsDialogOpen(true); } function openEditDialog(provider: AgentProviderConfig): void { setEditingProvider(provider); setForm({ name: provider.name, provider: "openclaw", gatewayUrl: provider.gatewayUrl, apiToken: "", isActive: provider.isActive, }); setFormError(null); setIsDialogOpen(true); } function closeDialog(): void { if (isSaving) { return; } setIsDialogOpen(false); setEditingProvider(null); setForm(INITIAL_FORM); setFormError(null); } async function handleSubmit(event: SyntheticEvent): Promise { event.preventDefault(); setFormError(null); setSuccessMessage(null); const validationError = validateForm(form, editingProvider !== null); if (validationError !== null) { setFormError(validationError); return; } const name = form.name.trim(); const gatewayUrl = form.gatewayUrl.trim(); const apiToken = form.apiToken.trim(); try { setIsSaving(true); if (editingProvider) { const updatePayload: UpdateAgentProviderRequest = { name, provider: form.provider, gatewayUrl, isActive: form.isActive, }; if (apiToken.length > 0) { updatePayload.credentials = { apiToken }; } await updateAgentProvider(editingProvider.id, updatePayload); setSuccessMessage(`Updated provider "${name}".`); } else { const createPayload: CreateAgentProviderRequest = { name, provider: form.provider, gatewayUrl, credentials: { apiToken }, isActive: form.isActive, }; await createAgentProvider(createPayload); setSuccessMessage(`Added provider "${name}".`); } setIsDialogOpen(false); setEditingProvider(null); setForm(INITIAL_FORM); await loadProviders(false); } catch (saveError: unknown) { setFormError(getErrorMessage(saveError, "Unable to save agent provider.")); } finally { setIsSaving(false); } } async function handleDeleteProvider(): Promise { if (!deleteTarget) { return; } try { setIsDeleting(true); await deleteAgentProvider(deleteTarget.id); setSuccessMessage(`Deleted provider "${deleteTarget.name}".`); setDeleteTarget(null); await loadProviders(false); } catch (deleteError: unknown) { setError(getErrorMessage(deleteError, "Failed to delete agent provider.")); } finally { setIsDeleting(false); } } return (

Agent Providers

Register OpenClaw gateways and API tokens used for external agent sessions.

OpenClaw Gateways Add one or more OpenClaw gateway endpoints and control which ones are active.
{error ? (

{error}

) : null} {successMessage ?

{successMessage}

: null} {isLoading ? (

Loading agent providers...

) : providers.length === 0 ? (

No agent providers configured yet. Add one to register an OpenClaw gateway.

) : ( providers.map((provider) => (

{provider.name}

{provider.isActive ? "Active" : "Inactive"} {provider.provider}

Gateway URL: {provider.gatewayUrl}

Created: {formatCreatedDate(provider.createdAt)}

)) )}
{ if (!nextOpen) { closeDialog(); return; } setIsDialogOpen(true); }} > {editingProvider ? "Edit Agent Provider" : "Add Agent Provider"} Configure an OpenClaw gateway URL and API token for agent provider registration.
void handleSubmit(event)} className="space-y-4">
) => { setForm((previous) => ({ ...previous, name: event.target.value })); }} placeholder="openclaw-primary" maxLength={100} disabled={isSaving} required />

Use letters, numbers, and hyphens only.

) => { setForm((previous) => ({ ...previous, gatewayUrl: event.target.value })); }} placeholder="https://my-openclaw.example.com" disabled={isSaving} required />
) => { setForm((previous) => ({ ...previous, apiToken: event.target.value })); }} placeholder={ editingProvider ? "Leave blank to keep existing token" : "Enter API token" } autoComplete="new-password" disabled={isSaving} />

{editingProvider ? "Leave blank to keep the currently stored token." : "Required when creating a provider."}

Inactive providers remain saved but are excluded from routing.

{ setForm((previous) => ({ ...previous, isActive: checked })); }} disabled={isSaving} />
{formError ? (

{formError}

) : null}
{ if (!open && !isDeleting) { setDeleteTarget(null); } }} > Delete Agent Provider Delete provider "{deleteTarget?.name}"? This permanently removes its gateway and token configuration. Cancel {isDeleting ? "Deleting..." : "Delete Provider"}
); }