/* Copyright 2026 Marimo. All rights reserved. */
import { useAtomValue } from "jotai";
import {
ChevronDown,
ChevronRight,
InfoIcon,
NotebookPenIcon,
PackageIcon,
SquareArrowOutUpRightIcon,
TerminalIcon,
} from "lucide-react";
import { Fragment, type JSX, useState } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import { ExternalLink } from "@/components/ui/links";
import { Tooltip } from "@/components/ui/tooltip";
import type { CellId } from "@/core/cells/ids";
import { resolvedMarimoConfigAtom } from "@/core/config/config";
import { renderHTML } from "@/plugins/core/RenderHTML";
import { splitMangledLocals } from "@/utils/local-variables";
import type { MarimoError } from "../../../core/kernel/messages";
import { cn } from "../../../utils/cn";
import { Alert, AlertTitle } from "../../ui/alert";
import { openPackageManager } from "../chrome/panels/packages-utils";
import { useChromeActions } from "../chrome/state";
import { AutoFixButton } from "../errors/auto-fix";
import { MangledSegments } from "../errors/mangled-local-chip";
import { CellLinkError } from "../links/cell-link";
import { processTextForUrls } from "./console/text-rendering";
const CollapsibleTraceback = ({
traceback,
}: {
traceback: string;
}): JSX.Element => {
const [isOpen, setIsOpen] = useState(true);
return (
{isOpen && (
{renderHTML({ html: traceback })}
)}
);
};
const Tip = (props: {
title?: string;
className?: string;
children: React.ReactNode;
}): JSX.Element => {
return (
{props.title ?? "Tip"}
{props.children}
);
};
interface Props {
cellId: CellId | undefined;
errors: MarimoError[];
className?: string;
}
/**
* List of errors due to violations of Marimo semantics.
*/
export const MarimoErrorOutput = ({
errors,
cellId,
className,
}: Props): JSX.Element => {
const chromeActions = useChromeActions();
// The console area (where tracebacks are shown) sits below the cell editor.
// When cell outputs are also displayed below the editor, the traceback is
// already adjacent, so the "See the console area" hint is redundant. It is
// only helpful when outputs are displayed above the editor.
const showConsoleHint =
useAtomValue(resolvedMarimoConfigAtom).display.cell_output === "above";
let titleContents: string | null =
"This cell wasn't run because it has errors";
let alertVariant: "destructive" | "default" = "destructive";
let titleColor = "text-error";
// A small muted overline shown above the title for static errors, where
// the cell genuinely never executes. These errors have no separate title:
// the body text already describes them, so the overline carries the status.
let statusOverline: string | null = null;
const liStyle = "my-0.5 ml-8 text-muted-foreground/40";
// Check for certain error types to adjust title and appearance
if (errors.some((e) => e.type === "interruption")) {
titleContents = "Interrupted";
} else if (errors.some((e) => e.type === "internal")) {
titleContents = "An internal error occurred";
} else if (errors.some((e) => e.type === "ancestor-prevented")) {
titleContents = "Ancestor prevented from running";
alertVariant = "default";
titleColor = "text-secondary-foreground";
} else if (errors.some((e) => e.type === "ancestor-stopped")) {
titleContents = "Ancestor stopped";
alertVariant = "default";
titleColor = "text-secondary-foreground";
} else if (errors.some((e) => e.type === "sql-error")) {
titleContents = "SQL error";
} else if (
errors.some(
(e) =>
e.type === "multiple-defs" ||
e.type === "cycle" ||
e.type === "setup-refs" ||
e.type === "import-star",
)
) {
// Static errors: the body text describes the problem, so there is no
// separate title — only the status overline.
titleContents = null;
statusOverline = "Cell not run";
} else {
// Check for exception type
const exceptionError = errors.find((e) => e.type === "exception");
if (exceptionError && "exception_type" in exceptionError) {
titleContents = exceptionError.exception_type;
}
}
// Group errors by type
const setupErrors = errors.filter(
(e): e is Extract =>
e.type === "setup-refs",
);
const cycleErrors = errors.filter(
(e): e is Extract => e.type === "cycle",
);
const multipleDefsErrors = errors.filter(
(e): e is Extract =>
e.type === "multiple-defs",
);
const importStarErrors = errors.filter(
(e): e is Extract =>
e.type === "import-star",
);
const interruptionErrors = errors.filter(
(e): e is Extract =>
e.type === "interruption",
);
const exceptionErrors = errors.filter(
(e): e is Extract =>
e.type === "exception",
);
const strictExceptionErrors = errors.filter(
(e): e is Extract =>
e.type === "strict-exception",
);
const internalErrors = errors.filter(
(e): e is Extract =>
e.type === "internal",
);
const ancestorPreventedErrors = errors.filter(
(e): e is Extract =>
e.type === "ancestor-prevented",
);
const ancestorStoppedErrors = errors.filter(
(e): e is Extract =>
e.type === "ancestor-stopped",
);
const syntaxErrors = errors.filter(
(e): e is Extract => e.type === "syntax",
);
const unknownErrors = errors.filter(
(e): e is Extract => e.type === "unknown",
);
const sqlErrors = errors.filter(
(e): e is Extract =>
e.type === "sql-error",
);
const openScratchpad = () => {
chromeActions.openApplication("scratchpad");
};
const renderMessages = () => {
const messages: JSX.Element[] = [];
if (syntaxErrors.length > 0 || unknownErrors.length > 0) {
// Detect if this is a pip/package manager related syntax error
const hasPipHint = syntaxErrors.some((error) =>
error.msg.includes("!pip"),
);
// Detect if this is a shell command syntax error (not pip)
const hasShellHint =
!hasPipHint &&
syntaxErrors.some((error) => error.msg.includes("use os.subprocess"));
messages.push(
The setup cell contains logic that must be run before any other
cell runs, including top-level imports used by top-level
functions. For this reason, it can't refer to other cells'
variables.
Try simplifying the setup cell to only contain only necessary
variables.
Learn more at our docs{" "}
.
,
);
}
if (cycleErrors.length > 0) {
messages.push(
An example of a cycle is if one cell declares a variable 'a' and
reads 'b', and another cell declares 'b' and and reads 'a'. Cycles
like this make it impossible for marimo to know how to run your
cells, and generally suggest that your code has a bug.
Try merging these cells into a single cell to eliminate the cycle.
marimo requires that each variable is defined in just one cell.
This constraint enables reactive and reproducible execution,
arbitrary cell reordering, seamless UI elements, execution as a
script, and more.
Try merging this cell with the mentioned cells or wrapping it in a
function. Alternatively, prefix variables with an underscore
(e.g., _{firstName}). to make them
private to this cell.
Learn more at our docs{" "}
.
to experiment without restrictions on variable names.
,
);
}
if (importStarErrors.length > 0) {
messages.push(
{importStarErrors.map((error, idx) => (
{error.msg}
))}
{cellId && (
)}
Star imports are incompatible with marimo's git-friendly file
format and reproducible reactive execution.
marimo's Python file format stores code in functions, so notebooks
can be imported as regular Python modules without executing all
their code. But Python disallows `import *` everywhere except at
the top-level of a module.
Star imports would also silently add names to globals, which would
be incompatible with reactive execution.
Learn more at our docs{" "}
.
,
);
}
if (interruptionErrors.length > 0) {
messages.push(
{interruptionErrors.map((_, idx) => (
{"This cell was interrupted and needs to be re-run."}
))}
{cellId && (
)}
,
);
}
if (exceptionErrors.length > 0) {
messages.push(
{processTextForUrls(error.msg, `exception-${idx}`)}
{"traceback" in error && error.traceback && (
)}
)}
);
})}
{exceptionErrors.some((e) => e.raising_cell != null) && (
Fix the error in the mentioned cells, or handle the exceptions
with try/except blocks.
)}
{cellId && }
,
);
}
if (strictExceptionErrors.length > 0) {
messages.push(
{strictExceptionErrors.map((error, idx) => (
{error.blamed_cell == null ? (
{error.msg}
) : (
{error.msg}
)}
))}
{cellId && (
)}
{strictExceptionErrors.some((e) => e.blamed_cell != null)
? "Ensure that the referenced cells define the required variables, or turn off strict execution."
: "Something is wrong with your declarations. Fix any discrepancies, or turn off strict execution."}
,
);
}
if (internalErrors.length > 0) {
messages.push(
{internalErrors.map((error, idx) => (
{error.msg}
))}
{cellId && }
,
);
}
if (ancestorPreventedErrors.length > 0) {
messages.push(