import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ArrowDownWideNarrow, Check, Download, Globe, Puzzle, RefreshCw, RotateCcw, Search, Trash2, TriangleAlert, } from "lucide-react"; import { Button } from "@/components/button"; import { Card } from "@/components/card"; import { Input } from "@/components/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/select"; import { EmptyState } from "@/components/empty-state"; import { TabStrip } from "@/components/tab-strip"; import { FakeSwitch } from "@/components/section-card"; import { Facts, GroupHeading, PANEL, PanelShell, } from "@/components/panel-layout"; import { InlineView, SurfaceScope } from "@/components/surface/surface-scope"; import { cn } from "@/lib/utils"; import { PluginIconBox } from "./plugin-bits"; import { PluginConsentPrompt } from "./PluginConsentPrompt"; import { PluginDetail } from "./PluginDetail"; import { categoriesOf, describeContributions, isSevere, matchesQuery, orderByRisk, sortPlugins, type PluginEntry, type SortKey, } from "./plugin-model"; import { usePluginsManager, type PluginsManager } from "./use-plugins-manager"; type Section = "installed" | "browse" | "updates"; export function PluginsTab() { return ( ); } function PluginsTabBody() { const { t } = useTranslation(); const manager = usePluginsManager(); const [section, setSection] = useState
("installed"); const [query, setQuery] = useState(""); const [detailId, setDetailId] = useState(null); const installed = manager.plugins.filter((p) => p.installed); const available = manager.plugins.filter((p) => p.inRegistry); const updates = installed.filter((p) => p.updateAvailable); const running = installed.filter((p) => p.status === "running").length; const detail = manager.plugins.find((p) => p.id === detailId) ?? null; const tabs = [ { id: "installed", label: t("plugins.manager.tabs.installed"), icon: , count: installed.length, }, { id: "browse", label: t("plugins.manager.tabs.browse"), icon: , }, { id: "updates", label: t("plugins.manager.tabs.updates"), icon: , count: updates.filter((p) => !p.pinnedVersion).length || undefined, }, ]; return ( <> } title={t("nav.plugins")} status={t("plugins.manager.status.summary", { installed: installed.length, running, })} scroll={false} actions={ } tabs={ setSection(id as Section)} /> } >
{manager.managedByServer && ( {t("plugins.manager.managedByServer")} )} {manager.registryError && ( {t("plugins.manager.registryUnreachable", { error: manager.registryError, })} )} {manager.loading ? (
) : section === "installed" ? ( ) : section === "browse" ? ( ) : ( )}
!open && setDetailId(null)} title={detail?.name ?? ""} icon={} width="wide" bare > {detail && ( setDetailId(null)} /> )} manager.setConsent(null)} onConfirm={() => void manager.confirmConsent()} /> ); } function Notice({ children }: { children: React.ReactNode }) { return (
{children}
); } function SortSelect({ value, onChange, }: { value: SortKey; onChange: (next: SortKey) => void; }) { const { t } = useTranslation(); const keys: SortKey[] = ["name", "updated", "category"]; return ( ); } function Toolbar({ query, onQuery, placeholder, children, }: { query: string; onQuery: (v: string) => void; placeholder: string; children?: React.ReactNode; }) { return (
onQuery(e.target.value)} placeholder={placeholder} className="h-8 pl-8 text-xs" />
{children}
); } function Group({ title, count, children, }: { title: string; count: number; children: React.ReactNode; }) { return (
{children}
); } function InstalledSection({ plugins, manager, query, onQuery, onOpen, }: { plugins: PluginEntry[]; manager: PluginsManager; query: string; onQuery: (v: string) => void; onOpen: (id: string) => void; }) { const { t } = useTranslation(); const [sort, setSort] = useState("name"); const rows = sortPlugins( plugins.filter((p) => matchesQuery(p, query)), sort, ); const running = rows.filter((p) => p.status === "running"); const stopped = rows.filter((p) => p.status !== "running"); const pending = plugins.filter((p) => p.updateAvailable && !p.pinnedVersion); return (
{rows.length === 0 ? ( ) : ( <> {running.length > 0 && ( {running.map((p) => ( onOpen(p.id)} /> ))} )} {stopped.length > 0 && ( {stopped.map((p) => ( onOpen(p.id)} /> ))} )} )}
); } function CardHead({ plugin, muted, facts, onOpen, }: { plugin: PluginEntry; muted?: boolean; facts: React.ReactNode; onOpen: () => void; }) { return ( ); } function InstalledCard({ plugin, manager, onOpen, }: { plugin: PluginEntry; manager: PluginsManager; onOpen: () => void; }) { const { t } = useTranslation(); const running = plugin.status === "running"; const failed = plugin.status === "failed" || plugin.status === "blocked"; const busy = manager.busy.has(plugin.id) || manager.busy.has("*"); const locked = manager.managedByServer; const adds = describeContributions(plugin.contributes) .slice(0, 3) .map((c) => t(`plugins.manager.contributes.${c.kind}`, { count: c.count })); return ( {plugin.author && {plugin.author}} v{plugin.version} {adds.length > 0 && {adds.join(", ")}} } /> {failed ? (
{plugin.status === "blocked" ? t("plugins.manager.status.blocked") : t("plugins.manager.status.failed")}
) : !running ? (
{t("plugins.manager.status.disabled")}
) : (
{plugin.pinnedVersion ? t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion, }) : t("plugins.manager.status.running")}
)}
{plugin.updateAvailable && !plugin.pinnedVersion && !failed && ( )} {failed ? ( ) : ( )}
); } function BrowseSection({ plugins, manager, query, onQuery, onOpen, }: { plugins: PluginEntry[]; manager: PluginsManager; query: string; onQuery: (v: string) => void; onOpen: (id: string) => void; }) { const { t } = useTranslation(); const [category, setCategory] = useState(null); const [sort, setSort] = useState("name"); const categories = useMemo(() => categoriesOf(plugins), [plugins]); const rows = sortPlugins( plugins.filter( (p) => matchesQuery(p, query) && (category === null || p.category === category), ), sort, ); return (
{categories.length > 0 && (
{[null, ...categories].map((c) => ( ))}
)} {rows.length === 0 ? ( ) : (
{rows.map((p) => ( onOpen(p.id)} /> ))}
)}
); } function BrowseCard({ plugin, manager, onOpen, }: { plugin: PluginEntry; manager: PluginsManager; onOpen: () => void; }) { const { t } = useTranslation(); const severe = orderByRisk(plugin.capabilities).filter(isSevere); const busy = manager.busy.has(plugin.id) || manager.busy.has("*"); return ( {plugin.author && {plugin.author}} {plugin.latestVersion && v{plugin.latestVersion}} {plugin.category && {plugin.category}} } />
{severe.length > 0 ? t(`plugins.capabilities.${severe[0]}.title`, { defaultValue: severe[0], nsSeparator: false, }) : t("plugins.manager.noServerAccess")} {plugin.installed ? ( {t("plugins.manager.installedBadge")} ) : ( )}
); } function UpdatesSection({ plugins, manager, onOpen, }: { plugins: PluginEntry[]; manager: PluginsManager; onOpen: (id: string) => void; }) { const { t } = useTranslation(); const pinned = plugins.filter((p) => p.pinnedVersion); const open = plugins.filter((p) => !p.pinnedVersion); const gated = open.filter((p) => p.addedCapabilities.length > 0); const plain = open.filter((p) => p.addedCapabilities.length === 0); if (plugins.length === 0) { return (
); } return (
{plain.length > 0 && ( {t("plugins.manager.updatesReady", { count: plain.length })} {gated.length > 0 && ` ${t("plugins.manager.updatesNeedReview", { count: gated.length })}`} )}
{[...gated, ...plain, ...pinned].map((p) => ( onOpen(p.id)} /> ))}
); } function UpdateRow({ plugin, manager, onOpen, }: { plugin: PluginEntry; manager: PluginsManager; onOpen: () => void; }) { const { t } = useTranslation(); const gated = plugin.addedCapabilities.length > 0; const busy = manager.busy.has(plugin.id) || manager.busy.has("*"); const notes = plugin.versions.find((v) => v.version === plugin.latestVersion); return (
{t("plugins.manager.versionChange", { from: plugin.version, to: plugin.latestVersion, })}
{plugin.pinnedVersion ? ( {t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion })} ) : gated ? ( {t("plugins.manager.updateAsksMore")} ) : null} {notes?.releaseNotesUrl && ( {t("plugins.manager.releaseNotes")} )}
{!plugin.pinnedVersion && ( )}
); }