/* Copyright 2026 Marimo. All rights reserved. */ import { zodResolver } from "@hookform/resolvers/zod"; import { useAtomValue } from "jotai"; import { BoxIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, DownloadCloudIcon, PackageCheckIcon, PackageXIcon, PlusIcon, RotateCwIcon, XIcon, } from "lucide-react"; import type React from "react"; import { useEffect, 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 { type EnvironmentOperationAlert, isEnvironmentOperationAlert, isMissingPackageAlert, useAlertActions, useAlerts, } from "@/core/alerts/state"; import { useResolvedMarimoConfig } from "@/core/config/config"; import type { PackageOperationStatus } from "@/core/kernel/messages"; import { useRequestClient } from "@/core/network/requests"; import { sandboxAtom } from "@/core/packages/sandbox-state"; import { RESTART_REQUIRED_DESCRIPTION } from "@/core/packages/toast-components"; import { isWasm } from "@/core/wasm/utils"; import { usePackageMetadata } from "@/hooks/usePackageMetadata"; import { Banner } from "@/plugins/impl/common/error-banner"; import { assertNever, 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"; import { useRestartKernel } from "./actions/useRestartKernel"; const RestartKernelButton = () => { const restartKernel = useRestartKernel(); return ; }; 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 } = useAlerts(); const { clearPackageAlert } = useAlertActions(); const [userConfig] = useResolvedMarimoConfig(); const sandbox = useAtomValue(sandboxAtom); const [desiredPackageVersions, setDesiredPackageVersions] = useState< Record >({}); const [selectedExtras, setSelectedExtras] = useState< Record >({}); const completedAlertId = packageAlert?.kind === "environment" && packageAlert.status.kind === "succeeded" && !packageAlert.restartRequired ? packageAlert.id : null; useEffect(() => { if (completedAlertId === null) { return; } const timeout = setTimeout( () => clearPackageAlert(completedAlertId), 10_000, ); return () => clearTimeout(timeout); }, [completedAlertId, clearPackageAlert]); if (packageAlert === null) { return null; } const sandboxPackageManager = packageAlert.source === "server" ? null : sandbox?.backend; const packageManager = sandboxPackageManager ?? userConfig.package_management.manager; const doesSupportVersioning = packageAlert.source === "server" || sandbox !== null || packageManager !== "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() && packageAlert.source !== "server" && ( <> with{" "} )} ) : (

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

)}
); } if (isEnvironmentOperationAlert(packageAlert)) { const { status, title, titleIcon, description } = getOperationStatusElements(packageAlert); return (
{titleIcon} {title}

{description}

{status !== "running" && packageAlert.restartRequired && ( <> {status !== "restart-required" && (

{RESTART_REQUIRED_DESCRIPTION}

)} )}
    {Object.entries(packageAlert.packages).map(([pkg, st], index) => (
  • {pkg}
  • ))}
{Object.keys(packageAlert.logs).length > 0 && ( )}
); } logNever(packageAlert); return null; }; function getOperationStatusElements({ action, status: outcome, restartRequired, }: EnvironmentOperationAlert) { const status: EnvironmentOperationAlert["status"] = outcome.kind === "succeeded" && restartRequired ? { kind: "restart-required", reason: RESTART_REQUIRED_DESCRIPTION } : outcome; const titles = { install: { running: "Installing packages", succeeded: "All packages installed!", failed: "Some packages failed to install", cancelled: "Package installation cancelled", }, remove: { running: "Removing packages", succeeded: "Packages removed", failed: "Some packages failed to remove", cancelled: "Package removal cancelled", }, }[action]; switch (status.kind) { case "running": return { status: "running", title: titles.running, titleIcon: , description: action === "install" ? "Installing packages:" : "Removing packages:", }; case "restart-required": return { status: "restart-required", title: "Changes saved — restart required", titleIcon: , description: status.reason, }; case "succeeded": return { status: "succeeded", title: titles.succeeded, titleIcon: , description: action === "install" ? "Installed packages:" : "Removed packages:", }; case "failed": return { status: "failed", title: titles.failed, titleIcon: , description: status.error, }; case "cancelled": return { status: "cancelled", title: titles.cancelled, titleIcon: , description: "The operation was interrupted. Some packages may have changed.", }; default: return assertNever(status); } } const ProgressIcon = ({ status, }: { status: PackageOperationStatus[string]; }) => { switch (status) { case "queued": return ; case "running": return ; case "succeeded": return ; case "failed": return ; case "restart-required": 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<{ fixedManager?: PackageManagerName; }> = ({ fixedManager }) => { 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={fixedManager ?? field.value} disabled={fixedManager !== undefined || 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"}
                
))}
)}
); };