/* Copyright 2026 Marimo. All rights reserved. */ import { BanIcon, MoreHorizontalIcon, RefreshCwIcon, WorkflowIcon, } from "lucide-react"; import { useDateFormatter } from "react-aria"; import { MultiIcon } from "@/components/icons/multi-icon"; import { Logger } from "@/utils/Logger"; import type { CellRuntimeState } from "../../../core/cells/types"; import { useElapsedTime } from "../../../hooks/useElapsedTime"; import { Tooltip } from "../../ui/tooltip"; import "./cell-status.css"; import { formatDistanceToNow } from "date-fns"; import { Time } from "@/utils/time"; export interface CellStatusComponentProps extends Pick< CellRuntimeState, "status" | "runStartTimestamp" | "interrupted" | "lastRunStartTimestamp" > { editing: boolean; edited: boolean; disabled: boolean; staleInputs: boolean; elapsedTime: number | null; uninstantiated: boolean; } export const CellStatusComponent: React.FC = ({ editing, status, disabled, staleInputs, edited, interrupted, elapsedTime, runStartTimestamp, lastRunStartTimestamp, uninstantiated, }) => { if (!editing) { return null; } const start = runStartTimestamp ?? lastRunStartTimestamp; const lastRanTime = start ? : null; // unexpected states if (disabled && status === "running") { Logger.error("CellStatusComponent: disabled and running"); return null; } // stale and disabled by self if (disabled && staleInputs) { return ( This cell is stale, but it's disabled and can't be run {lastRanTime} } usePortal={true} >
); } // disabled, but not stale if (disabled) { return ( This cell is disabled {lastRanTime} } usePortal={true} >
); } // disabled from parent if (!staleInputs && status === "disabled-transitively") { return ( An ancestor of this cell is disabled, so it can't be run {lastRanTime} } usePortal={true} >
); } // stale from parent being disabled if (staleInputs && status === "disabled-transitively") { return ( This cell is stale, but an ancestor is disabled so it can't be run {lastRanTime} } usePortal={true} >
); } // running & queued icons get priority over edited/interrupted if (status === "running") { return ( This cell is running {lastRanTime} } usePortal={true} >
); } // queued if (status === "queued") { return ( This cell is queued to run {lastRanTime} } usePortal={true} >
); } // outdated: cell needs to be re-run if (edited || interrupted || staleInputs || uninstantiated) { const elapsedTimeStr = formatElapsedTime(elapsedTime); const elapsedTimeComponent = elapsedTime ? ( ) : null; // Customize tooltips based on why the cell needs to be re-run let title = ""; let timerTitle: React.ReactNode = ""; if (uninstantiated) { title = "This cell has not yet been run"; } else if (interrupted) { title = "This cell was interrupted when it was last run"; timerTitle = ( This cell ran for {elapsedTimeComponent} before being interrupted ); } else if (edited) { title = "This cell has been modified since it was last run"; timerTitle = This cell took {elapsedTimeComponent} to run; } else { // staleInputs title = "This cell has not been run with the latest inputs"; timerTitle = This cell took {elapsedTimeComponent} to run; } return (
{elapsedTime && ( {timerTitle} {lastRanTime}
} usePortal={true} >
{elapsedTimeStr}
)} ); } // either running or finished if (elapsedTime !== null) { const elapsedTimeStr = formatElapsedTime(elapsedTime); const elapsedTimeComponent = elapsedTime ? ( ) : null; return ( This cell took {elapsedTimeComponent} to run {lastRanTime} } usePortal={true} >
{elapsedTimeStr}
); } // default return null; }; export const ElapsedTime = (props: { elapsedTime: string }) => { return ( {props.elapsedTime} ); }; const LastRanTime = (props: { lastRanTime: number }) => { const date = new Date(props.lastRanTime * 1000); const today = new Date(); // Looks like HH:MM:SS.SSS AM/PM const timeFormatter = useDateFormatter({ hour: "numeric", minute: "numeric", second: "numeric", fractionalSecondDigits: 3, hour12: true, }); // Looks like MM/DD HH:MM:SS.SSS AM/PM const dateTimeFormatter = useDateFormatter({ month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric", fractionalSecondDigits: 3, hour12: true, }); const formatter = date.toDateString() === today.toDateString() ? timeFormatter : dateTimeFormatter; return ( Ran at{" "} {formatter.format(date)} {" "} ({formatDistanceToNow(date)} ago) ); }; export function formatElapsedTime(elapsedTime: number | null) { if (elapsedTime === null) { return ""; } const milliseconds = elapsedTime; const seconds = milliseconds / 1000; if (seconds >= 60) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}m${remainingSeconds}s`; } if (seconds >= 1) { return `${seconds.toFixed(2).toString()}s`; } return `${milliseconds.toFixed(0).toString()}ms`; } const CellTimer = (props: { startTime: Time }) => { const time = useElapsedTime(props.startTime.toMilliseconds()); return {formatElapsedTime(time)}; };