/* Copyright 2026 Marimo. All rights reserved. */ import { zodResolver } from "@hookform/resolvers/zod"; import { BoxIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, DownloadCloudIcon, PackageCheckIcon, PackageXIcon, PlusIcon, XIcon, } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { useForm } from "react-hook-form"; import type { z } from "zod"; import { Form, FormControl, FormField, FormItem, FormMessage, } from "@/components/ui/form"; import { isInstallingPackageAlert, isMissingPackageAlert, useAlertActions, useAlerts, } from "@/core/alerts/state"; import { useResolvedMarimoConfig } from "@/core/config/config"; import type { PackageInstallationStatus } from "@/core/kernel/messages"; import { useRequestClient } from "@/core/network/requests"; import { isWasm } from "@/core/wasm/utils"; import { usePackageMetadata } from "@/hooks/usePackageMetadata"; import { Banner } from "@/plugins/impl/common/error-banner"; import { logNever } from "@/utils/assertNever"; import { cn } from "@/utils/cn"; import { Logger } from "@/utils/Logger"; import { type PackageManagerName, PackageManagerNames, type UserConfig, UserConfigSchema, } from "../../core/config/config-schema"; import { getDirtyValues } from "../app-config/get-dirty-values"; import { Button } from "../ui/button"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "../ui/dropdown-menu"; import { ExternalLink } from "../ui/links"; import { NativeSelect } from "../ui/native-select"; import { Tooltip } from "../ui/tooltip"; function parsePackageSpecifier(spec: string): { name: string; extras: string[]; } { const match = spec.match(/^([^[]+)(?:\[([^\]]+)])?$/); if (!match) { return { name: spec, extras: [] }; } const [, name, extrasStr] = match; const extras = extrasStr ? extrasStr.split(",").map((e) => e.trim()) : []; return { name, extras }; } function buildPackageSpecifier(name: string, extras: string[]): string { if (extras.length === 0) { return name; } return `${name}[${extras.join(",")}]`; } const SourceBadge: React.FC<{ source?: "kernel" | "server" }> = ({ source, }) => { if (source !== "server") { return null; } return ( server ); }; export const PackageAlert: React.FC = () => { const { packageAlert, packageLogs } = useAlerts(); const { clearPackageAlert } = useAlertActions(); const [userConfig] = useResolvedMarimoConfig(); const [desiredPackageVersions, setDesiredPackageVersions] = useState< Record >({}); const [selectedExtras, setSelectedExtras] = useState< Record >({}); if (packageAlert === null) { return null; } const doesSupportVersioning = userConfig.package_management.manager !== "pixi"; if (isMissingPackageAlert(packageAlert)) { return (
Missing packages

The following packages were not found:

{packageAlert.packages.map((pkg, index) => { const parsed = parsePackageSpecifier(pkg); const currentExtras = selectedExtras[pkg] || parsed.extras; return ( {doesSupportVersioning && ( )} ); })}
setSelectedExtras((prev) => ({ ...prev, [pkg]: extras, })) } /> setDesiredPackageVersions((prev) => ({ ...prev, [pkg]: value, })) } packageName={parsed.name} />
{packageAlert.isolated ? ( <> { const parsed = parsePackageSpecifier(pkg); const currentExtras = selectedExtras[pkg] || parsed.extras; return buildPackageSpecifier(parsed.name, currentExtras); })} versions={desiredPackageVersions} clearPackageAlert={() => clearPackageAlert(packageAlert.id)} source={packageAlert.source} /> {!isWasm() && ( <> with{" "} )} ) : (

If you set up a{" "} virtual environment , marimo can install these packages for you.

)}
); } if (isInstallingPackageAlert(packageAlert)) { const { status, title, titleIcon, description } = getInstallationStatusElements(packageAlert.packages); if (status === "installed") { setTimeout(() => clearPackageAlert(packageAlert.id), 10_000); } return (
{titleIcon} {title}

{description}

    {Object.entries(packageAlert.packages).map(([pkg, st], index) => (
  • {pkg}
  • ))}
{Object.keys(packageLogs).length > 0 && ( )}
); } logNever(packageAlert); return null; }; function getInstallationStatusElements(packages: PackageInstallationStatus) { const statuses = new Set(Object.values(packages)); const status = statuses.has("queued") || statuses.has("installing") ? "installing" : statuses.has("failed") ? "failed" : "installed"; if (status === "installing") { return { status: "installing", title: "Installing packages", titleIcon: , description: "Installing packages:", }; } if (status === "installed") { return { status: "installed", title: "All packages installed!", titleIcon: , description: "Installed packages:", }; } return { status: "failed", title: "Some packages failed to install", titleIcon: , description: "See error logs.", }; } const ProgressIcon = ({ status, }: { status: PackageInstallationStatus[string]; }) => { switch (status) { case "queued": return ; case "installing": return ; case "installed": return ; case "failed": return ; default: logNever(status); return null; } }; const InstallPackagesButton = ({ manager, packages, versions, clearPackageAlert, source, }: { manager: PackageManagerName; packages: string[]; versions: Record; clearPackageAlert: () => void; source?: "kernel" | "server"; }) => { const { sendInstallMissingPackages } = useRequestClient(); return ( ); }; const PackageManagerForm: React.FC = () => { const [config, setConfig] = useResolvedMarimoConfig(); const { saveUserConfig } = useRequestClient(); // Create form const form = useForm({ resolver: zodResolver( UserConfigSchema as unknown as z.ZodType, ), defaultValues: config, }); const onSubmit = async (values: UserConfig) => { // Only send values that were actually changed to avoid // overwriting backend values the form doesn't manage const dirtyValues = getDirtyValues(values, form.formState.dirtyFields); if (Object.keys(dirtyValues).length === 0) { return; // Nothing changed } await saveUserConfig({ config: dirtyValues }).then(() => { // Update local state with form values setConfig((prev) => ({ ...prev, ...values })); }); }; return (
( field.onChange(e.target.value)} value={field.value} disabled={field.disabled} className="inline-flex mr-2" > {PackageManagerNames.map((option) => ( ))} )} />
); }; interface PackageVersionSelectProps { value: string; onChange: (value: string) => void; packageName: string; } interface ExtrasSelectorProps { packageName: string; selectedExtras: string[]; onExtrasChange: (extras: string[]) => void; } const ExtrasSelector: React.FC = ({ packageName, selectedExtras, onExtrasChange, }) => { const [isOpen, setIsOpen] = useState(false); const { isPending, error, data: pkgMeta } = usePackageMetadata(packageName); const handleExtraToggle = (extra: string, checked: boolean) => { if (checked) { onExtrasChange([...selectedExtras, extra]); } else { onExtrasChange(selectedExtras.filter((e) => e !== extra)); } }; const canSelectExtras = !isPending && !error; const availableExtras = (pkgMeta?.extras ?? []).filter( // Filter out common development-only extras like "dev" and "test". (extra) => !/^(dev|test|testing)$/i.test(extra), ); return (
{packageName} {selectedExtras.length > 0 ? ( [ { if (canSelectExtras) { setIsOpen(open); } }} > {selectedExtras.length > 0 && (
{selectedExtras.map((extra) => ( handleExtraToggle(extra, false)} > {extra} ))}
)}
{availableExtras.map((extra) => ( { handleExtraToggle(extra, checked); }} className="font-mono text-sm" onSelect={(e) => e.preventDefault()} > {extra} ))}
]
) : availableExtras.length > 0 ? ( { if (canSelectExtras) { setIsOpen(open); } }} >
Package extras
{availableExtras.map((extra) => ( { handleExtraToggle(extra, checked); }} className="font-mono text-sm" onSelect={(e) => e.preventDefault()} > {extra} ))}
) : null}
); }; const PackageVersionSelect: React.FC = ({ value, onChange, packageName, }) => { const { error, isPending, data: pkgMeta } = usePackageMetadata(packageName); if (error) { return ( onChange(e.target.value)} disabled={true} className="inline-flex ml-2 w-24 text-ellipsis" > ); } return ( onChange(e.target.value)} disabled={isPending} className="inline-flex ml-2 w-24 text-ellipsis" > {isPending ? ( ) : ( ["latest", ...pkgMeta.versions.slice(0, 100)].map((version) => ( )) )} ); }; interface StreamingLogsViewerProps { packageLogs: { [packageName: string]: string }; } const StreamingLogsViewer: React.FC = ({ packageLogs, }) => { const [isExpanded, setIsExpanded] = useState(true); const packageCount = Object.keys(packageLogs).length; if (packageCount === 0) { return null; } return (
{isExpanded && (
{Object.entries(packageLogs).map(([packageName, logs]) => (

{packageName}

                  {logs || "No logs available"}
                
))}
)}
); };