diff --git a/src/components/settings/opencode-plugins-modal.tsx b/src/components/settings/opencode-plugins-modal.tsx new file mode 100644 index 0000000..e5dccd2 --- /dev/null +++ b/src/components/settings/opencode-plugins-modal.tsx @@ -0,0 +1,264 @@ +"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 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.opencodePlugins") + 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(() => { + logEndRef.current?.scrollIntoView({ behavior: "smooth" }) + }, [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 = crypto.randomUUID() + await stream.start(taskId) + try { + await opencodeInstallPlugins(taskId) + } catch { + // Error handled by event stream + } + }, [stream]) + + const handleInstallOne = useCallback( + async (name: string) => { + const taskId = crypto.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 + + return ( + + + + {t("title")} + + +
+ {summary && ( +
+
Config: {summary.config_path}
+
Cache: {summary.cache_dir}
+
+ )} + + {loading && !summary ? ( +
+ +
+ ) : summary && summary.plugins.length > 0 ? ( +
+
+ {t("declared")} +
+ {summary.plugins.map((plugin: PluginInfo) => ( +
+
+
+ {plugin.declared_spec} +
+
+ + {t(`status.${plugin.status}`)} + + {plugin.installed_version && ( + + v{plugin.installed_version} + + )} +
+
+
+ {plugin.status === "missing" ? ( + + ) : ( + + )} +
+
+ ))} +
+ ) : summary ? ( +
+ {t("noPlugins")} +
+ ) : null} + + {summary && summary.plugins.length > 0 && ( +
+ + +
+ )} + + {stream.status !== "idle" && ( +
+ {stream.logs.map((line, i) => ( +
+ {line} +
+ ))} +
+
+ )} + + {stream.status === "success" && ( +
+ {t("success")} +
+ )} + {stream.status === "failed" && ( +
+ {t("failed")}: {stream.error} +
+ )} +
+ +
+ ) +} diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index d6c9674..162f618 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "تكوين مزود واجهة برمجة التطبيقات وبيانات اعتماد Cline. يتم حفظ الإعدادات في ~/.cline/data/." + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": { diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index b624c70..cc5a43c 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "Konfigurieren Sie den Cline API-Anbieter und die Anmeldedaten. Einstellungen werden in ~/.cline/data/ gespeichert." + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": { diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 7a0aed9..2daf012 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "Configure Cline API provider and credentials. Settings are saved to ~/.cline/data/." + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": { diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 8c2f68a..4c2d488 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "Configure el proveedor de API y las credenciales de Cline. La configuración se guarda en ~/.cline/data/." + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": { diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 10cba72..f41df62 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "Configurez le fournisseur API et les identifiants Cline. Les paramètres sont enregistrés dans ~/.cline/data/." + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": { diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index c92bec0..1efcc7f 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "Cline API プロバイダーと認証情報を設定します。設定は ~/.cline/data/ に保存されます。" + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": { diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 5b30ea0..c0d72cb 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "Cline API 제공자와 자격 증명을 구성합니다. 설정은 ~/.cline/data/에 저장됩니다." + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": { diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index d2db0a9..36945d1 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "Configure o provedor de API e as credenciais do Cline. As configurações são salvas em ~/.cline/data/." + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": { diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index dfe6102..d3037bd 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "配置 Cline API 提供商和凭证。设置将保存到 ~/.cline/data/。" + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": { diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 07c3998..953cb01 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -685,6 +685,21 @@ }, "cline": { "configDescription": "配置 Cline API 提供商和憑證。設定將儲存到 ~/.cline/data/。" + }, + "opencodePlugins": { + "title": "OpenCode Plugins", + "declared": "Declared Plugins", + "noPlugins": "No plugins declared.", + "status": { + "installed": "Installed", + "missing": "Missing" + }, + "installAll": "Install All Missing", + "install": "Install", + "uninstall": "Uninstall", + "refresh": "Refresh", + "success": "All plugins installed successfully.", + "failed": "Installation failed" } }, "SettingsPages": {