/* 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 { isConnectedAtom } from "@/core/network/connection"; import type { DependencyTreeNode, DependencyTreeResponse, } from "@/core/network/types"; import { sandboxSyncAtom } from "@/core/packages/sandbox-state"; import { stripPackageManagerPrefix } from "@/core/packages/package-input-utils"; import { usePackageAction } from "@/core/packages/usePackageAction"; import { usePackageDependencies } from "@/core/packages/usePackageDependencies"; import { useInstallPackages } from "@/core/packages/useInstallPackage"; import { isWasm } from "@/core/wasm/utils"; import { ErrorBanner } from "@/plugins/impl/common/error-banner"; import { cn } from "@/utils/cn"; import { copyToClipboard } from "@/utils/copy"; import { Events } from "@/utils/events"; import { RuntimeDetails } from "./runtime-panel"; import { PanelEmptyState } from "./empty-state"; import { PACKAGES_INPUT_ID, packagesToInstallAtom } from "./packages-utils"; type ViewMode = "tree" | "list"; type PackageInstallationContext = DependencyTreeResponse["context"]; 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 connected = useAtomValue(isConnectedAtom); return (
{connected && }
); }; const PackageContents: React.FC = () => { const syncing = useAtomValue(sandboxSyncAtom).kind === "running"; const [userViewMode, setUserViewMode] = React.useState(null); const { data: dependencies, error, isPending } = usePackageDependencies(); // Only show on the first load if (isPending) { return ( Loading installed packages… ); } if (error) { return ; } const isTreeSupported = dependencies.tree != null; const name = dependencies.tree?.name; const version = dependencies?.tree?.version; const sandboxBackend = dependencies.context.kind === "sandbox" ? dependencies.context.backend : null; const isSandbox = sandboxBackend !== null; const viewMode = isSandbox ? "tree" : resolveViewMode(userViewMode, isTreeSupported); const scopeLabel = name && name !== "" ? "project" : "environment"; return (
{isTreeSupported && !isSandbox && (
{scopeLabel}
{name && ( {name} {version && ` v${version}`} )}
)}
{syncing && (

Showing packages from the last sync.

)} {viewMode === "list" ? ( ) : ( )}
); }; export default PackagesPanel; const InstallPackageForm: React.FC<{ context: PackageInstallationContext; }> = ({ context }) => { const [input, setInput] = React.useState(""); const { handleClick: openSettings } = useOpenSettingsToTab(); const isSandbox = context.kind === "sandbox"; const packageManager = isSandbox ? context.backend : context.name; // 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 = () => { setInput(""); }; const installPackages = () => { const cleanedInput = stripPackageManagerPrefix(input); handleInstallPackages( [cleanedInput], // the backend will handle splitting the packages onSuccessInstallPackages, ); }; return (
) : isSandbox ? (
} > ); }; const PackagesList: React.FC<{ packages: { name: string; version: string }[]; }> = ({ packages }) => { // Sort case-insensitively so packages are strictly alphabetical // regardless of capitalization (package managers sort inconsistently). const sortedPackages = React.useMemo( () => packages.toSorted((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), ), [packages], ); if (packages.length === 0) { return ( } /> ); } return ( Name Version {sortedPackages.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 }[]; }> = ({ packageName, tags }) => { const { loading, run } = usePackageAction("upgrade", packageName, tags); if (isWasm()) { return null; } return ( Upgrade ); }; const RemoveButton: React.FC<{ packageName: string; tags?: { kind: string; value: string }[]; }> = ({ packageName, tags }) => { const { loading, run } = usePackageAction("remove", packageName, tags); if (packageName.toLowerCase() === "marimo") { return null; } return ( Remove ); }; const DependencyTree: React.FC<{ tree: DependencyTreeNode | null; error?: Error | null; sandboxBackend: "uv" | "pixi" | null; }> = ({ tree, error, sandboxBackend }) => { 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) { if (sandboxBackend === "pixi") { return ( } /> ); } 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; }> = ({ nodeId, node, level, isTopLevel = false, expandedNodes, onToggle }) => { 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 === "dedupe") { return (
already in tree
); } 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"; }