import { useCallback, useEffect, useState } from "react"; import { ExternalLink, RefreshCw, Puzzle, Trash2 } from "lucide-react"; import type { Translations } from "@/i18n/types"; import { Link } from "react-router-dom"; import { api } from "@/lib/api"; import type { HubAgentPluginRow, PluginsHubResponse } from "@/lib/api"; import { Button } from "@nous-research/ui/ui/components/button"; import { Badge } from "@nous-research/ui/ui/components/badge"; import { Select, SelectOption } from "@nous-research/ui/ui/components/select"; import { Switch } from "@nous-research/ui/ui/components/switch"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; import { CommandBlock } from "@nous-research/ui/ui/components/command-block"; import { H2 } from "@/components/NouiTypography"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useToast } from "@/hooks/useToast"; import { Toast } from "@/components/Toast"; import { useI18n } from "@/i18n"; import { PluginSlot } from "@/plugins"; import { cn } from "@/lib/utils"; /** Select value for built-in memory (`config` uses empty string). Never use `""` — UI Select maps empty value to an empty label. */ const MEMORY_PROVIDER_BUILTIN = "__hermes_memory_builtin__"; export default function PluginsPage() { const [hub, setHub] = useState(null); const [loading, setLoading] = useState(true); const [installId, setInstallId] = useState(""); const [installForce, setInstallForce] = useState(false); const [installEnable, setInstallEnable] = useState(true); const [installBusy, setInstallBusy] = useState(false); const [rescanBusy, setRescanBusy] = useState(false); const [memorySel, setMemorySel] = useState(MEMORY_PROVIDER_BUILTIN); const [contextSel, setContextSel] = useState("compressor"); const [providerBusy, setProviderBusy] = useState(false); const [rowBusy, setRowBusy] = useState(null); const { toast, showToast } = useToast(); const { t } = useI18n(); const loadHub = useCallback(() => { return api .getPluginsHub() .then((h) => { setHub(h); const p = h.providers; setMemorySel(p.memory_provider ? p.memory_provider : MEMORY_PROVIDER_BUILTIN); setContextSel(p.context_engine || "compressor"); }) .catch(() => showToast(t.common.loading, "error")); }, [showToast, t.common.loading]); useEffect(() => { setLoading(true); void loadHub().finally(() => setLoading(false)); }, [loadHub]); const onInstall = async () => { const id = installId.trim(); if (!id) { showToast(t.pluginsPage.installHint, "error"); return; } setInstallBusy(true); try { const r = await api.installAgentPlugin({ identifier: id, force: installForce, enable: installEnable, }); showToast(`${r.plugin_name ?? id} installed`, "success"); if ((r.warnings?.length ?? 0) > 0) showToast(r.warnings!.join(" "), "error"); if ((r.missing_env?.length ?? 0) > 0) showToast(`${t.pluginsPage.missingEnvWarn} ${r.missing_env!.join(", ")}`, "error"); setInstallId(""); await loadHub(); } catch (e) { showToast(e instanceof Error ? e.message : "Install failed", "error"); } finally { setInstallBusy(false); } }; const onRescan = async () => { setRescanBusy(true); try { const rc = await api.rescanPlugins(); showToast( `${t.pluginsPage.refreshDashboard} (${rc.count})`, "success", ); await loadHub(); } catch (e) { showToast(e instanceof Error ? e.message : "Rescan failed", "error"); } finally { setRescanBusy(false); } }; const onSaveProviders = async () => { setProviderBusy(true); try { await api.savePluginProviders({ memory_provider: memorySel === MEMORY_PROVIDER_BUILTIN ? "" : memorySel, context_engine: contextSel, }); showToast(t.pluginsPage.savedProviders, "success"); await loadHub(); } catch (e) { showToast(e instanceof Error ? e.message : "Save failed", "error"); } finally { setProviderBusy(false); } }; const setRuntimeLoading = async (name: string, fn: () => Promise) => { setRowBusy(name); try { await fn(); await loadHub(); } catch (e) { showToast(e instanceof Error ? e.message : "Failed", "error"); } finally { setRowBusy(null); } }; const rows = hub?.plugins ?? []; const providers = hub?.providers; return (

{t.app.nav.plugins}

{t.pluginsPage.headline}

{providers && ( {t.pluginsPage.providersHeading}

{t.pluginsPage.providersHint}

)} {t.pluginsPage.installHeading}

{t.pluginsPage.installHint}

setInstallId(e.target.value)} />
{t.pluginsPage.forceReinstall}
{t.pluginsPage.enableAfterInstall}

{t.pluginsPage.rescanHint}

{t.pluginsPage.removeHint}

{t.pluginsPage.pluginListHeading}

{loading ? (
{t.common.loading}
) : rows.length === 0 ? (

{t.common.noResults}

) : (
    {rows.map((row: HubAgentPluginRow) => (
  • ))}
)}
{(hub?.orphan_dashboard_plugins?.length ?? 0) > 0 ? (

{t.pluginsPage.orphanHeading}

    {hub!.orphan_dashboard_plugins.map((m) => (
  • {m.label ?? m.name} — {m.description || m.tab?.path} {!m.tab?.hidden ? ( {t.pluginsPage.openTab} ) : null}
  • ))}
) : null}
); } interface PluginRowCardProps { row: HubAgentPluginRow; rowBusy: string | null; setRuntimeLoading: ( name: string, fn: () => Promise, ) => Promise; showToast: (msg: string, variant: "success" | "error") => void; t: Translations; } function PluginRowCard(props: PluginRowCardProps) { const { row, rowBusy, setRuntimeLoading, showToast, t, } = props; const dm = row.dashboard_manifest; const tabPath = dm?.tab && !dm.tab.hidden ? dm.tab.override ?? dm.tab.path : null; const busy = rowBusy === row.name; const badgeTone = row.runtime_status === "enabled" ? "success" : row.runtime_status === "disabled" ? "destructive" : "outline"; return (
{row.name} {t.pluginsPage.sourceBadge}: {row.source} v{row.version || "—"} {row.runtime_status} {row.auth_required ? ( {t.pluginsPage.authRequired} ) : null}
{row.description ? (

{row.description}

) : null}
{tabPath ? ( {t.pluginsPage.openTab} ) : null} {row.can_update_git ? ( ) : null} {row.can_remove ? ( ) : null}
{dm?.slots?.length ? (

{t.pluginsPage.dashboardSlots}: {dm.slots.join(", ")}

) : null} {row.auth_required ? ( ) : null} {!row.has_dashboard_manifest && !dm ? (

{t.pluginsPage.noDashboardTab}

) : null}
); }