"use client" import { useEffect, useState } from "react" import { AlertTriangle, Loader2 } from "lucide-react" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" import { toast } from "sonner" import { ResponsiveDialog, ResponsiveDialogContent, ResponsiveDialogDescription, ResponsiveDialogFooter, ResponsiveDialogHeader, ResponsiveDialogTitle, } from "@/components/ui/responsive-dialog" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { previewPlugin, reloadPlugin, type PluginPreview, type PluginSummary, type SecretDefinition, } from "@/lib/api" import { useOfficialPlugins } from "@/hooks/use-official-plugins" interface PluginUpdateDialogProps { plugin: PluginSummary | null open: boolean onOpenChange: (open: boolean) => void onUpdated: (updated: PluginSummary) => void } export function PluginUpdateDialog({ plugin, open, onOpenChange, onUpdated, }: PluginUpdateDialogProps) { const { byId } = useOfficialPlugins() const manifestUrl = plugin ? byId.get(plugin.id)?.manifest_url ?? null : null const [newManifestPreview, setNewManifestPreview] = useState(null) const [previewLoading, setPreviewLoading] = useState(false) const [updating, setUpdating] = useState(false) const [error, setError] = useState(null) useEffect(() => { if (!open || !plugin) { setNewManifestPreview(null) setError(null) return } if (!manifestUrl) { setNewManifestPreview(null) return } let cancelled = false setPreviewLoading(true) previewPlugin(manifestUrl) .then((preview) => { if (!cancelled) setNewManifestPreview(preview) }) .catch(() => { if (!cancelled) setNewManifestPreview(null) }) .finally(() => { if (!cancelled) setPreviewLoading(false) }) return () => { cancelled = true } }, [open, plugin, manifestUrl]) if (!plugin) return null const installedKeys = new Set( plugin.required_secrets.map((secret) => secret.key), ) const newRequiredSecrets: SecretDefinition[] = newManifestPreview?.required_secrets.filter( (secret) => !installedKeys.has(secret.key), ) ?? [] const pendingReleases = [...(plugin.pending_releases ?? [])] const hasReleaseNotes = pendingReleases.length > 0 const hasLatestVersion = Boolean(plugin.latest_version) const handleUpdate = async () => { if (!manifestUrl) { setError("No manifest URL available for this plugin.") return } setUpdating(true) setError(null) try { const updated = await reloadPlugin(plugin.id, { url: manifestUrl }) if (plugin.latest_version) { try { window.localStorage.setItem( `happyview:plugin-update-seen:${plugin.id}`, plugin.latest_version, ) } catch { // ignore storage errors } } onUpdated(updated) onOpenChange(false) toast.success( `Updated ${plugin.name} to v${plugin.latest_version ?? updated.version}`, ) } catch (err) { const message = err instanceof Error ? err.message : "Failed to update plugin" setError(message) } finally { setUpdating(false) } } const updateDisabled = updating || previewLoading || !manifestUrl return ( Update {plugin.name}
v{plugin.version} → v{plugin.latest_version ?? "unknown"}
{newRequiredSecrets.length > 0 && (

This update requires new configuration. After updating, you'll need to set these secrets.

    {newRequiredSecrets.map((secret) => (
  • {secret.name} {" "} ({secret.key})
  • ))}
)} {hasReleaseNotes ? (
{pendingReleases.map((release) => (

v{release.version} ·{" "} {new Date(release.published_at).toLocaleDateString()}

{release.body}
))}
) : (

No release notes available.

)} {error && (
{error}
)}
) }