/* Copyright 2026 Marimo. All rights reserved. */ import { type DOMNode, Element, Text } from "html-react-parser"; import { useAtomValue } from "jotai"; import { BugPlayIcon, ChevronDown, CopyIcon, ExternalLinkIcon, MessageCircleIcon, SearchIcon, } from "lucide-react"; import { type JSX, useState } from "react"; import { Accordion, AccordionContent, AccordionItem, } from "@/components/ui/accordion"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Kbd } from "@/components/ui/kbd"; import { Tooltip } from "@/components/ui/tooltip"; import { getCellEditorView } from "@/core/cells/cells"; import type { CellId } from "@/core/cells/ids"; import { SCRATCH_CELL_ID } from "@/core/cells/ids"; import { insertDebuggerAtLine } from "@/core/codemirror/editing/debugging"; import { aiEnabledAtom } from "@/core/config/config"; import { getRequestClient } from "@/core/network/requests"; import { isStaticNotebook } from "@/core/static/static-state"; import { isWasm } from "@/core/wasm/utils"; import { renderHTML } from "@/plugins/core/RenderHTML"; import { sanitizeHtml } from "@/plugins/core/sanitize-html"; import { copyToClipboard } from "@/utils/copy"; import { elementContainsMarimoCellFile, extractAllTracebackInfo, getTracebackInfo, } from "@/utils/traceback"; import { cn } from "../../../utils/cn"; import { AIFixButton } from "../errors/auto-fix"; import { CellLinkTraceback } from "../links/cell-link"; import type { OnRefactorWithAI } from "../Output"; interface Props { cellId: CellId | undefined; traceback: string; onRefactorWithAI?: OnRefactorWithAI; } const KEY = "item"; /** * List of errors due to violations of Marimo semantics. */ export const MarimoTracebackOutput = ({ onRefactorWithAI, traceback, cellId, }: Props): JSX.Element => { const htmlTraceback = renderHTML({ html: traceback, additionalReplacements: [replaceTracebackFilenames, replaceTracebackPrefix], }); const [expanded, setExpanded] = useState(true); const lastTracebackLine = lastLine(traceback); const aiEnabled = useAtomValue(aiEnabledAtom); // Get last traceback info const tracebackInfo = extractAllTracebackInfo(traceback)?.at(0); // Don't show in wasm, static notebooks, or scratchpad const showDebugger = tracebackInfo && tracebackInfo.kind === "cell" && !isWasm() && !isStaticNotebook() && cellId !== SCRATCH_CELL_ID; const showAIFix = onRefactorWithAI && aiEnabled && !isStaticNotebook(); const showSearch = !isStaticNotebook(); const handleRefactorWithAI = (triggerImmediately: boolean) => { onRefactorWithAI?.({ prompt: `My code gives the following error:\n\n${lastTracebackLine}`, triggerImmediately, }); }; const [error, errorMessage] = lastTracebackLine.split(":", 2); return (
setExpanded((prev) => !prev)} >
{error || "Error"}:{" "} {errorMessage}
{htmlTraceback}
{showAIFix && ( handleRefactorWithAI(false)} applyAutofix={() => handleRefactorWithAI(true)} /> )} {showDebugger && ( )} {showSearch && ( Search on Google Ask in Discord { // Strip HTML from the traceback (sanitize first to prevent XSS) const div = document.createElement("div"); div.innerHTML = sanitizeHtml(traceback); const textContent = div.textContent || ""; copyToClipboard(textContent); }} > Copy to clipboard )}
); }; function lastLine(text: string): string { const el = document.createElement("div"); el.innerHTML = sanitizeHtml(text); const lines = el.textContent?.split("\n").filter(Boolean); return lines?.at(-1) || ""; } export const replaceTracebackFilenames = (domNode: DOMNode) => { const info = getTracebackInfo(domNode); if (info?.kind === "cell") { const tooltipContent = ; return ( {!isWasm() && ( )} ); } if (info?.kind === "file") { return (
{ getRequestClient().openFile({ path: info.filePath, lineNumber: info.lineNumber, }); }} > "{info.filePath}"
); } }; export const replaceTracebackPrefix = (domNode: DOMNode) => { if ( domNode instanceof Text && domNode.nodeValue?.includes("File") && domNode.next instanceof Element && elementContainsMarimoCellFile(domNode.next) ) { return <>{domNode.nodeValue.replace("File", "Cell")}; } }; const InsertBreakpointContent = () => { return ( <> Insert a breakpoint() at this line ); };