/* Copyright 2026 Marimo. All rights reserved. */ import { type DOMNode, Element, Text } from "html-react-parser"; import { useAtomValue } from "jotai"; import { BugPlayIcon, ChevronDown, ChevronRight, CopyIcon, ExternalLinkIcon, MessageCircleIcon, SearchIcon, } from "lucide-react"; import { type JSX, useState } from "react"; 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 { toggleBreakpoint } from "@/core/codemirror/cells/debugger-state"; import { insertDebuggerAtLine } from "@/core/codemirror/editing/debugging"; import { aiFeaturesEnabledAtom } from "@/core/config/config"; import { getFeatureFlag } from "@/core/config/feature-flag"; 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 { containsMangledLocal, splitMangledLocals, } from "@/utils/local-variables"; import { elementContainsMarimoCellFile, extractAllTracebackInfo, getTracebackInfo, } from "@/utils/traceback"; import { useOpenAiAssistant } from "../chrome/wrapper/useOpenAiAssistant"; import { AIFixButton, buildFixInChatPrompt } from "../errors/auto-fix"; import { MangledSegments } from "../errors/mangled-local-chip"; import { CellLinkTraceback } from "../links/cell-link"; import type { OnRefactorWithAI } from "../Output"; interface Props { cellId: CellId | undefined; traceback: string; onRefactorWithAI?: OnRefactorWithAI; } /** * 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, replaceMangledLocal, ], }); const lastTracebackLine = lastLine(traceback); const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom); const openAiAssistant = useOpenAiAssistant(); // 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 && aiFeaturesEnabled && !isStaticNotebook(); const showSearch = !isStaticNotebook(); const [isOpen, setIsOpen] = useState(true); const handleRefactorWithAI = (triggerImmediately: boolean) => { onRefactorWithAI?.({ prompt: `My code gives the following error:\n\n${lastTracebackLine}`, triggerImmediately, }); }; const openAISidebar = () => { openAiAssistant({ prompt: buildFixInChatPrompt(cellId, lastTracebackLine), }); }; return (
{isOpen && (
{htmlTraceback}
)}
{showAIFix && ( handleRefactorWithAI(false)} applyAutofix={() => handleRefactorWithAI(true)} openChat={openAISidebar} /> )} {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") { // With the live debugger enabled, toggle a real gutter breakpoint // instead of mutating the user's code with a `breakpoint()` statement. const debuggerEnabled = getFeatureFlag("debugger"); const tooltipContent = debuggerEnabled ? ( ) : ( ); return ( {!isWasm() && ( )} ); } if (info?.kind === "file") { return (
{ getRequestClient().openFile({ path: info.filePath, lineNumber: info.lineNumber, }); }} > "{info.filePath}"
); } }; /** * Replace any cell-local mangled name (`_cell__`) inside a text * node with a {@link MangledLocalChip}. The mangled name appears in both * the final `NameError:` line and inside compiled-cell source lines because * the compiler rewrites underscore-prefixed references at AST-visit time. */ export const replaceMangledLocal = (domNode: DOMNode) => { if (!(domNode instanceof Text) || !domNode.nodeValue) { return; } if (!containsMangledLocal(domNode.nodeValue)) { return; } const segments = splitMangledLocals(domNode.nodeValue); return ; }; 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 ); }; const ToggleBreakpointContent = () => { return <>Toggle a breakpoint at this line; };