"use client" import { useCallback, useEffect, useRef, useState } from "react" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Download, Loader2, RefreshCw, Trash2 } from "lucide-react" import { useTranslations } from "next-intl" import { opencodeListPlugins, opencodeInstallPlugins, opencodeUninstallPlugin, } from "@/lib/api" import { usePluginInstallStream } from "@/hooks/use-plugin-install-stream" import { randomUUID } from "@/lib/utils" import type { PluginCheckSummary, PluginInfo } from "@/lib/types" interface OpencodePluginsModalProps { open: boolean onOpenChange: (open: boolean) => void onCompleted?: () => void } export function OpencodePluginsModal({ open, onOpenChange, onCompleted, }: OpencodePluginsModalProps) { const t = useTranslations("AcpAgentSettings") const [summary, setSummary] = useState(null) const [loading, setLoading] = useState(false) const [uninstalling, setUninstalling] = useState(null) const stream = usePluginInstallStream() const logEndRef = useRef(null) const isOperating = stream.status === "running" || uninstalling !== null const refresh = useCallback(async () => { setLoading(true) try { const result = await opencodeListPlugins() setSummary(result) } catch (err) { console.error("[OpencodePlugins] Failed to list plugins:", err) } finally { setLoading(false) } }, []) useEffect(() => { if (open) { refresh() stream.reset() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]) useEffect(() => { const container = logEndRef.current?.parentElement if (container) { container.scrollTop = container.scrollHeight } }, [stream.logs]) useEffect(() => { if (stream.status === "success" || stream.status === "failed") { refresh() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [stream.status]) const handleInstallAll = useCallback(async () => { const taskId = randomUUID() await stream.start(taskId) try { await opencodeInstallPlugins(taskId) } catch { // Error handled by event stream } }, [stream]) const handleInstallOne = useCallback( async (name: string) => { const taskId = randomUUID() await stream.start(taskId) try { await opencodeInstallPlugins(taskId, [name]) } catch { // Error handled by event stream } }, [stream] ) const handleUninstall = useCallback(async (name: string) => { setUninstalling(name) try { const result = await opencodeUninstallPlugin(name) setSummary(result) } catch (err) { console.error("[OpencodePlugins] Uninstall failed:", err) } finally { setUninstalling(null) } }, []) const handleClose = useCallback( (nextOpen: boolean) => { onOpenChange(nextOpen) if (!nextOpen) { onCompleted?.() } }, [onOpenChange, onCompleted] ) const missingCount = summary?.plugins.filter((p) => p.status === "missing").length ?? 0 const floatingCount = summary?.plugins.filter((p) => p.declared_spec.endsWith("@latest")) .length ?? 0 const hasActionablePlugins = missingCount > 0 || floatingCount > 0 return ( {t("opencodePlugins.title")}
{summary && (
Config: {summary.config_path}
Cache: {summary.cache_dir}
)} {loading && !summary ? (
) : summary && summary.plugins.length > 0 ? (
{t("opencodePlugins.declared")}
{summary.plugins.map((plugin: PluginInfo) => (
{plugin.declared_spec}
{t(`opencodePlugins.status.${plugin.status}`)} {plugin.installed_version && ( v{plugin.installed_version} )}
{plugin.status === "missing" ? ( ) : ( )}
))}
) : summary ? (
{t("opencodePlugins.noPlugins")}
) : null} {summary && summary.plugins.length > 0 && (
)} {stream.status !== "idle" && (
{stream.logs.map((line, i) => (
{line}
))}
)} {stream.status === "success" && (
{t("opencodePlugins.success")}
)} {stream.status === "failed" && (
{t("opencodePlugins.failed")}: {stream.error}
)}
) }