/* Copyright 2026 Marimo. All rights reserved. */ import { useAtomValue, useSetAtom } from "jotai"; import { BoxIcon, ChevronDownIcon, ChevronRightIcon, HelpCircleIcon, } from "lucide-react"; import React from "react"; import { useOpenSettingsToTab } from "@/components/app-config/state"; import { Spinner } from "@/components/icons/spinner"; import { SearchInput } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tooltip } from "@/components/ui/tooltip"; import { toast } from "@/components/ui/use-toast"; import { useResolvedMarimoConfig } from "@/core/config/config"; import { useRequestClient } from "@/core/network/requests"; import type { DependencyTreeNode } from "@/core/network/types"; import { stripPackageManagerPrefix } from "@/core/packages/package-input-utils"; import { showRemovePackageToast, showUpgradePackageToast, } from "@/core/packages/toast-components"; import { useInstallPackages } from "@/core/packages/useInstallPackage"; import { isWasm } from "@/core/wasm/utils"; import { useAsyncData } from "@/hooks/useAsyncData"; import { ErrorBanner } from "@/plugins/impl/common/error-banner"; import { cn } from "@/utils/cn"; import { copyToClipboard } from "@/utils/copy"; import { Events } from "@/utils/events"; import { PanelEmptyState } from "./empty-state"; import { PACKAGES_INPUT_ID, packagesToInstallAtom } from "./packages-utils"; type ViewMode = "tree" | "list"; const PackageActionButton: React.FC<{ onClick: () => void; loading: boolean; children: React.ReactNode; className?: string; }> = ({ onClick, loading, children, className }) => { if (loading) { return ; } return ( ); }; const PackagesPanel: React.FC = () => { const [config] = useResolvedMarimoConfig(); const packageManager = config.package_management.manager; const { getDependencyTree, getPackageList } = useRequestClient(); const [userViewMode, setUserViewMode] = React.useState(null); const { data: dependencies, error, refetch, isPending, } = useAsyncData(async () => { const [listPackagesResponse, dependencyTreeResponse] = await Promise.all([ getPackageList(), getDependencyTree(), ]); return { list: listPackagesResponse.packages, tree: dependencyTreeResponse.tree, }; }, [packageManager]); // Only show on the first load if (isPending) { return ; } if (error) { return ; } const isTreeSupported = dependencies.tree != null; const viewMode = resolveViewMode(userViewMode, isTreeSupported); const name = dependencies.tree?.name; const version = dependencies?.tree?.version; const isSandbox = name === ""; // name is the project name otherwise return (
{isTreeSupported && (
{isSandbox ? "sandbox" : "project"}
{name && !isSandbox && ( {name} {version && ` v${version}`} )}
)} {viewMode === "list" ? ( ) : ( )}
); }; export default PackagesPanel; const InstallPackageForm: React.FC<{ packageManager: string; onSuccess: () => void; }> = ({ onSuccess, packageManager }) => { const [input, setInput] = React.useState(""); const { handleClick: openSettings } = useOpenSettingsToTab(); // Get the packages to install from the atom const packagesToInstall = useAtomValue(packagesToInstallAtom); const setPackagesToInstall = useSetAtom(packagesToInstallAtom); // Set the input value when packagesToInstall changes React.useEffect(() => { if (packagesToInstall) { setInput(packagesToInstall); // Clear the atom after setting the input setPackagesToInstall(null); } }, [packagesToInstall, setPackagesToInstall]); const { loading, handleInstallPackages } = useInstallPackages(); const onSuccessInstallPackages = () => { onSuccess(); setInput(""); }; const installPackages = () => { const cleanedInput = stripPackageManagerPrefix(input); handleInstallPackages( [cleanedInput], // the backend will handle splitting the packages onSuccessInstallPackages, ); }; return (
) : ( openSettings("packageManagementAndData")} className="mr-2 h-4 w-4 shrink-0 opacity-50 hover:opacity-80 cursor-pointer" /> ) } rootClassName="flex-1 border-none" value={input} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); installPackages(); } }} onChange={(e) => setInput(e.target.value)} /> Packages are installed using the package manager specified in your user configuration. Depending on your package manager, you can install packages with various formats:
Package name: A package name; this will install the latest version.
Example: httpx
Package and version: {" "} A package with a specific version or version range.
{"Examples: httpx==0.27.0, httpx>=0.27.0"}
Git: A Git repository
Example: git+https://github.com/encode/httpx
URL: A remote wheel or source distribution.
Example: https://example.com/httpx-0.27.0.tar.gz
Path: A local wheel, source distribution, or project directory.
Example: /example/foo-0.1.0-py3-none-any.whl
} > ); }; const PackagesList: React.FC<{ onSuccess: () => void; packages: { name: string; version: string }[]; }> = ({ onSuccess, packages }) => { if (packages.length === 0) { return ( } /> ); } return ( Name Version {packages.map((item) => ( { await copyToClipboard(`${item.name}==${item.version}`); toast({ title: "Copied to clipboard", }); }} > {item.name} {item.version} ))}
); }; const UpgradeButton: React.FC<{ packageName: string; tags?: { kind: string; value: string }[]; onSuccess: () => void; }> = ({ packageName, tags, onSuccess }) => { const [loading, setLoading] = React.useState(false); const { addPackage } = useRequestClient(); // Hide upgrade button in WASM if (isWasm()) { return null; } const handleUpgradePackage = async () => { try { setLoading(true); const group = tags?.find((tag) => tag.kind === "group")?.value; const response = await addPackage({ package: packageName, upgrade: true, group, }); if (response.success) { onSuccess(); showUpgradePackageToast(packageName); } else { showUpgradePackageToast(packageName, response.error); } } finally { setLoading(false); } }; return ( Upgrade ); }; const RemoveButton: React.FC<{ packageName: string; tags?: { kind: string; value: string }[]; onSuccess: () => void; }> = ({ packageName, tags, onSuccess }) => { const [loading, setLoading] = React.useState(false); const { removePackage } = useRequestClient(); const handleRemovePackage = async () => { try { setLoading(true); const group = tags?.find((tag) => tag.kind === "group")?.value; const response = await removePackage({ package: packageName, group, }); if (response.success) { onSuccess(); showRemovePackageToast(packageName); } else { showRemovePackageToast(packageName, response.error); } } finally { setLoading(false); } }; return ( Remove ); }; const DependencyTree: React.FC<{ tree: DependencyTreeNode | null; error?: Error | null; onSuccess: () => void; }> = ({ tree, error, onSuccess }) => { const [expandedNodes, setExpandedNodes] = React.useState>( new Set(), ); // Reset tree to collapsed state when tree data changes (including refetches) React.useEffect(() => { setExpandedNodes(new Set()); }, [tree]); if (error) { return ; } if (!tree) { return ; } if (tree.dependencies.length === 0) { return ( } /> ); } const toggleNode = (nodeId: string) => { setExpandedNodes((prev) => { const newSet = new Set(prev); if (newSet.has(nodeId)) { newSet.delete(nodeId); } else { newSet.add(nodeId); } return newSet; }); }; return (
{tree.dependencies.map((dep, index) => (
))}
); }; const DependencyTreeNode: React.FC<{ nodeId: string; node: DependencyTreeNode; level: number; isTopLevel?: boolean; expandedNodes: Set; onToggle: (nodeId: string) => void; onSuccess: () => void; }> = ({ nodeId, node, level, isTopLevel = false, expandedNodes, onToggle, onSuccess, }) => { const hasChildren = node.dependencies.length > 0; const isExpanded = expandedNodes.has(nodeId); const indent = isTopLevel ? 0 : 16 + level * 16; // Top-level uses CSS padding, children use calculated indent const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (hasChildren) { onToggle(nodeId); } } // Allow arrow keys to bubble up for tree navigation }; const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); if (hasChildren) { onToggle(nodeId); } }; return (
{/* Expand/collapse arrow */} {hasChildren ? ( isExpanded ? ( ) : ( ) ) : (
)} {/* Package info */}
{node.name} {node.version && ( v{node.version} )}
{/* Tags */}
{node.tags.map((tag, index) => { if (tag.kind === "cycle") { return (
cycle
); } if (tag.kind === "extra") { return (
{tag.value}
); } if (tag.kind === "group") { return (
{tag.value}
); } return null; })}
{/* Actions for top-level packages */} {isTopLevel && (
)}
{/* Children */} {hasChildren && isExpanded && (
{node.dependencies.map((child, index) => ( ))}
)}
); }; function resolveViewMode( userViewMode: ViewMode | null, isTreeSupported: boolean, ): ViewMode { if (userViewMode === "list") { return "list"; } if (isTreeSupported) { return userViewMode || "tree"; } return "list"; }