/* Copyright 2026 Marimo. All rights reserved. */ import { atom, useAtomValue, useSetAtom } from "jotai"; import { ActivityIcon, ChevronDown, ChevronRight, CircleCheck, CircleEllipsis, CirclePlayIcon, CircleX, } from "lucide-react"; import React, { type JSX, Suspense, useEffect, useRef, useState } from "react"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { useVegaEmbed } from "react-vega"; import useResizeObserver from "use-resize-observer"; import { compile } from "vega-lite"; import { Tooltip } from "@/components/ui/tooltip"; import { useCellIds } from "@/core/cells/cells"; import type { CellId } from "@/core/cells/ids"; import { formatLogTimestamp } from "@/core/cells/logs"; import { type CellRun, type Run, type RunId, runsAtom, useRunsActions, } from "@/core/cells/runs"; import { type ResolvedTheme, useTheme } from "@/theme/useTheme"; import { cn } from "@/utils/cn"; import { ClearButton } from "../buttons/clear-button"; import type { SignalListener } from "../charts/types"; import { ElapsedTime, formatElapsedTime } from "../editor/cell/CellStatus"; import { PanelEmptyState } from "../editor/chrome/panels/empty-state"; import { usePanelSection } from "../editor/chrome/panels/panel-context"; import { CellLink } from "../editor/links/cell-link"; import { type ChartPosition, type ChartValues, createGanttBaseSpec, VEGA_HOVER_SIGNAL, } from "./tracing-spec"; import { formatChartTime } from "./utils"; const expandedRunsAtom = atom>(new Map()); export const Tracing: React.FC = () => { const { runIds: newestToOldestRunIds, runMap } = useAtomValue(runsAtom); const expandedRuns = useAtomValue(expandedRunsAtom); const { clearRuns } = useRunsActions(); const { theme } = useTheme(); const [chartPosition, setChartPosition] = useState("above"); const panelSection = usePanelSection(); const toggleChartPosition = () => { if (chartPosition === "above") { setChartPosition("sideBySide"); } else { setChartPosition("above"); } }; if (newestToOldestRunIds.length === 0) { return ( Cells that have ran will appear here.} icon={} /> ); } const tracingComponent = (
{newestToOldestRunIds.map((runId: RunId, index: number) => { const run = runMap.get(runId); if (run) { return ( ); } return null; })}
); if (panelSection === "sidebar") { return tracingComponent; } // Allow the panel to be resized when in the wider developer panel return ( {tracingComponent}
); }; interface VegaHoverCellSignal { cell: string[]; vlPoint: unknown; } const TraceBlock: React.FC<{ run: Run; /** * undefined means the user hasn't clicked on this run yet */ isExpanded: boolean | undefined; isMostRecentRun: boolean; chartPosition: ChartPosition; theme: ResolvedTheme; }> = ({ run, isMostRecentRun, chartPosition, isExpanded, theme }) => { const setExpandedRuns = useSetAtom(expandedRunsAtom); // We prefer the user's last click, but if they haven't clicked on this run, // we expand the most recent run by default, otherwise we collapse it. isExpanded = isExpanded ?? isMostRecentRun; const onToggleExpanded = () => { setExpandedRuns((prev) => { const newMap = new Map(prev); newMap.set(run.runId, !isExpanded); return newMap; }); }; const Icon = isExpanded ? ChevronDown : ChevronRight; const chevron = ; const traceTitle = ( Run - {formatLogTimestamp(run.runStartTime)} {chevron} ); if (!isExpanded) { return (
{traceTitle}
); } return ( ); }; const TraceBlockBody: React.FC<{ run: Run; chartPosition: ChartPosition; theme: ResolvedTheme; title: React.ReactNode; }> = ({ run, chartPosition, theme, title }) => { const [hoveredCellId, setHoveredCellId] = useState(); const vegaRef = useRef(null); const { ref, width = 300 } = useResizeObserver(); const cellIds = useCellIds(); const chartValues: ChartValues[] = [...run.cellRuns.values()].map( (cellRun) => { const elapsedTime = cellRun.elapsedTime ?? 0; return { cell: cellRun.cellId, cellNum: cellIds.inOrderIds.indexOf(cellRun.cellId), startTimestamp: formatChartTime(cellRun.startTime), endTimestamp: formatChartTime(cellRun.startTime + elapsedTime), elapsedTime: formatElapsedTime(elapsedTime * 1000), status: cellRun.status, }; }, ); const hiddenInputElementId = `hiddenInputElement-${run.runId}`; const vegaSpec = compile( createGanttBaseSpec( chartValues, hiddenInputElementId, chartPosition, theme, ), ).spec; const embed = useVegaEmbed({ ref: vegaRef, spec: vegaSpec, options: { theme: theme === "dark" ? "dark" : undefined, width: width - 50, height: chartPosition === "above" ? 120 : 100, actions: false, // Using vega instead of vegaLite as some parts of the spec get interpreted as vega & will throw warnings mode: "vega", renderer: "canvas", }, }); useEffect(() => { const signalListeners: SignalListener[] = [ { signalName: VEGA_HOVER_SIGNAL, handler: (_name: string, value: unknown) => { const signalValue = value as VegaHoverCellSignal; const hoveredCell = (signalValue.cell?.[0] ?? undefined) as | CellId | undefined; setHoveredCellId(hoveredCell ?? null); }, }, ]; signalListeners.forEach(({ signalName, handler }) => { embed?.view.addSignalListener(signalName, handler); }); return () => { signalListeners.forEach(({ signalName, handler }) => { embed?.view.removeSignalListener(signalName, handler); }); }; }, [embed]); const traceRows = ( ); const chartElement = (
); if (chartPosition === "above") { return (
          {title}
          {chartElement}
          {traceRows}
        
); } return (
        {title}
        {traceRows}
      
{chartElement}
); }; const TraceRows = (props: { run: Run; hoveredCellId: CellId | null | undefined; hiddenInputElementId: string; }) => { const { run, hoveredCellId, hiddenInputElementId } = props; // To send signals to Vega from React, we bind a hidden input element const hiddenInputRef = useRef(null); const dispatchHoverEvent = (cellId: CellId | null) => { // dispatch input event to trigger vega's param to update if (hiddenInputRef.current) { hiddenInputRef.current.value = String(cellId); hiddenInputRef.current.dispatchEvent( new Event("input", { bubbles: true }), ); } }; return (
{[...run.cellRuns.values()].map((cellRun) => ( ))}
); }; const StatusIcons: Record = { success: , running: , error: , queued: , }; interface TraceRowProps { cellRun: CellRun; hovered: boolean; dispatchHoverEvent: (cellId: CellId | null) => void; } const TraceRow: React.FC = ({ cellRun, hovered, dispatchHoverEvent, }: TraceRowProps) => { const elapsedTimeStr = cellRun.elapsedTime ? formatElapsedTime(cellRun.elapsedTime * 1000) : "-"; const elapsedTimeTooltip = cellRun.elapsedTime ? ( This cell took to run ) : ( This cell has not been run ); const handleMouseEnter = () => { dispatchHoverEvent(cellRun.cellId); }; const handleMouseLeave = () => { dispatchHoverEvent(null); }; return (
[{formatLogTimestamp(cellRun.startTime)}] () {cellRun.code}
{elapsedTimeStr} {StatusIcons[cellRun.status]}
); };