diff --git a/dashboard/src/app/extensions/plugins/page.tsx b/dashboard/src/app/extensions/plugins/page.tsx index 6b1b90f..4a678f9 100644 --- a/dashboard/src/app/extensions/plugins/page.tsx +++ b/dashboard/src/app/extensions/plugins/page.tsx @@ -1,13 +1,18 @@ 'use client'; import { useState, useMemo, useEffect } from 'react'; +import useSWR from 'swr'; import { toast } from '@/components/toast'; -import { type Plugin } from '@/lib/api'; +import { type Plugin, getInstalledPlugins, updatePlugin, type InstalledPluginInfo } from '@/lib/api'; import { AlertCircle, + ArrowUpCircle, Check, + Download, + ExternalLink, GitBranch, Loader, + Package, Plus, RefreshCw, Search, @@ -488,6 +493,204 @@ function PluginFormModal({ ); } +// ───────────────────────────────────────────────────────────────────────────── +// Installed Plugins Section (discovered from OpenCode config) +// ───────────────────────────────────────────────────────────────────────────── + +function InstalledPluginCard({ + plugin, + onUpdate, + updating, +}: { + plugin: InstalledPluginInfo; + onUpdate: (packageName: string) => void; + updating: string | null; +}) { + const isUpdating = updating === plugin.package; + + return ( +
+
+
+
+ +
+
+

{plugin.package}

+
+ + {plugin.installed_version ?? 'unknown'} + + {plugin.update_available && plugin.latest_version && ( + <> + + {plugin.latest_version} available + + )} +
+
+
+ +
+ + + + {plugin.update_available && ( + + )} + {!plugin.update_available && plugin.latest_version && ( + + + Latest + + )} +
+
+
+ ); +} + +function InstalledPluginsSection() { + const { data, isLoading, error, mutate } = useSWR( + 'installed-plugins', + getInstalledPlugins, + { revalidateOnFocus: false } + ); + + const [updating, setUpdating] = useState(null); + const [updateProgress, setUpdateProgress] = useState(null); + + const handleUpdate = (packageName: string) => { + setUpdating(packageName); + setUpdateProgress(null); + + const cleanup = updatePlugin(packageName, (event) => { + setUpdateProgress(event.message); + + if (event.event_type === 'complete') { + toast.success(event.message); + setUpdating(null); + setUpdateProgress(null); + mutate(); // Refresh the list + } else if (event.event_type === 'error') { + toast.error(event.message); + setUpdating(null); + setUpdateProgress(null); + } + }); + + // Cleanup on unmount + return cleanup; + }; + + if (isLoading) { + return ( +
+
+ + Loading installed plugins... +
+
+ ); + } + + if (error) { + return ( +
+
+ + Failed to load installed plugins +
+
+ ); + } + + const plugins = data?.plugins ?? []; + + if (plugins.length === 0) { + return ( +
+

+ No plugins installed in OpenCode. Plugins are added via the plugin array in ~/.config/opencode/opencode.json. +

+
+ ); + } + + const hasUpdates = plugins.some((p) => p.update_available); + + return ( +
+
+
+

Installed OpenCode Plugins

+

+ Plugins discovered from your OpenCode config +

+
+
+ {hasUpdates && ( + + + Updates available + + )} + +
+
+ + {updateProgress && ( +
+

{updateProgress}

+
+ )} + +
+ {plugins.map((plugin) => ( + + ))} +
+
+ ); +} + export default function PluginsPage() { const { status, @@ -714,10 +917,17 @@ export default function PluginsPage() { )} + {/* Installed OpenCode Plugins - discovered from config */} + + + {/* Divider */} +
+ + {/* Library Plugins */}
-

Plugins

-

Manage OpenCode plugins stored in your library repo.

+

Library Plugins

+

Plugins managed in your library repo (synced to OpenCode).

diff --git a/dashboard/src/lib/api.ts b/dashboard/src/lib/api.ts index e7331df..8aa64df 100644 --- a/dashboard/src/lib/api.ts +++ b/dashboard/src/lib/api.ts @@ -1621,6 +1621,59 @@ export async function saveLibraryPlugins( await ensureLibraryResponse(res, "Failed to save plugins"); } +// ───────────────────────────────────────────────────────────────────────────── +// Installed OpenCode Plugins (discovered from OpenCode config) +// ───────────────────────────────────────────────────────────────────────────── + +export interface InstalledPluginInfo { + package: string; + spec: string; + installed_version: string | null; + latest_version: string | null; + update_available: boolean; +} + +export interface InstalledPluginsResponse { + plugins: InstalledPluginInfo[]; +} + +// Get installed plugins from OpenCode config with version info +export async function getInstalledPlugins(): Promise { + const res = await apiFetch("/api/system/plugins/installed"); + if (!res.ok) { + throw new Error("Failed to fetch installed plugins"); + } + return res.json(); +} + +// Update a plugin (returns SSE stream) +export function updatePlugin( + packageName: string, + onEvent: (event: { event_type: string; message: string; progress?: number }) => void +): () => void { + const url = `${window.location.origin}/api/system/plugins/${encodeURIComponent(packageName)}/update`; + + const eventSource = new EventSource(url, { withCredentials: true }); + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + onEvent(data); + if (data.event_type === "complete" || data.event_type === "error") { + eventSource.close(); + } + } catch (e) { + console.error("Failed to parse SSE event:", e); + } + }; + + eventSource.onerror = () => { + eventSource.close(); + }; + + return () => eventSource.close(); +} + // ───────────────────────────────────────────────────────────────────────────── // Rules // ─────────────────────────────────────────────────────────────────────────────