/* Copyright 2026 Marimo. All rights reserved. */ import type { EditorView } from "@codemirror/view"; import { useAtomValue, useSetAtom } from "jotai"; import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsDownIcon, ChevronsUpIcon, ChevronUpIcon, Code2Icon, Columns2Icon, DatabaseIcon, EyeIcon, EyeOffIcon, ImageIcon, LinkIcon, PlayIcon, PlusCircleIcon, ScissorsIcon, SparklesIcon, TextCursorInputIcon, Trash2Icon, XCircleIcon, ZapIcon, ZapOffIcon, } from "lucide-react"; import { MultiIcon } from "@/components/icons/multi-icon"; import { useImperativeModal } from "@/components/modal/ImperativeModal"; import { DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { toast } from "@/components/ui/use-toast"; import { aiCompletionCellAtom } from "@/core/ai/state"; import { maybeAddMarimoImport } from "@/core/cells/add-missing-import"; import { hasOnlyOneCellAtom, useCellActions } from "@/core/cells/cells"; import { type CellId, SETUP_CELL_ID } from "@/core/cells/ids"; import type { CellData } from "@/core/cells/types"; import { formatEditorViews } from "@/core/codemirror/format"; import { getCurrentLanguageAdapter, toggleToLanguage, } from "@/core/codemirror/language/commands"; import { switchLanguage } from "@/core/codemirror/language/extension"; import { MARKDOWN_INITIAL_HIDE_CODE } from "@/core/codemirror/language/languages/markdown"; import { aiEnabledAtom, appWidthAtom, autoInstantiateAtom, } from "@/core/config/config"; import { kioskModeAtom } from "@/core/mode"; import { useRequestClient } from "@/core/network/requests"; import type { CellConfig, RuntimeState } from "@/core/network/types"; import { canLinkToCell, createCellLink } from "@/utils/cell-urls"; import { copyToClipboard } from "@/utils/copy"; import { downloadCellOutputAsImage } from "@/utils/download"; import { MarkdownIcon, PythonIcon } from "../cell/code/icons"; import { useDeleteCellCallback } from "../cell/useDeleteCell"; import { useRunCell } from "../cell/useRunCells"; import { useSplitCellCallback } from "../cell/useSplitCell"; import { NameCellInput } from "./name-cell-input"; import type { ActionButton } from "./types"; export interface CellActionButtonProps extends Pick< CellData, "name" | "config" > { cellId: CellId; status: RuntimeState; hasOutput: boolean; hasConsoleOutput: boolean; getEditorView: () => EditorView | null; } interface Props { cell: CellActionButtonProps | null; closePopover?: () => void; } export function useCellActionButtons({ cell, closePopover }: Props) { const { createNewCell: createCell, updateCellConfig, updateCellName, moveCell, sendToTop, sendToBottom, addColumnBreakpoint, clearCellOutput, markUntouched, } = useCellActions(); const splitCell = useSplitCellCallback(); const runCell = useRunCell(cell?.cellId); const hasOnlyOneCell = useAtomValue(hasOnlyOneCellAtom); const canDelete = !hasOnlyOneCell; const deleteCell = useDeleteCellCallback(); const { openModal } = useImperativeModal(); const setAiCompletionCell = useSetAtom(aiCompletionCellAtom); const aiEnabled = useAtomValue(aiEnabledAtom); const autoInstantiate = useAtomValue(autoInstantiateAtom); const kioskMode = useAtomValue(kioskModeAtom); const appWidth = useAtomValue(appWidthAtom); const { saveCellConfig } = useRequestClient(); if (!cell || kioskMode) { return []; } const { cellId, config, getEditorView, name, hasOutput, hasConsoleOutput, status, } = cell; const toggleDisabled = async () => { const newConfig = { disabled: !config.disabled }; await saveCellConfig({ configs: { [cellId]: newConfig } }); updateCellConfig({ cellId, config: newConfig }); }; const toggleHideCode = async () => { const newConfig: Partial = { hide_code: !config.hide_code }; await saveCellConfig({ configs: { [cellId]: newConfig } }); updateCellConfig({ cellId, config: newConfig }); const editorView = getEditorView(); // If we're hiding the code, we should blur the editor // otherwise, we should focus it if (editorView) { if (newConfig.hide_code) { editorView.contentDOM.blur(); } else { editorView.focus(); } } }; const isSetupCell = cellId === SETUP_CELL_ID; // Actions const actions: ActionButton[][] = [ [ { icon: , label: "Run cell", hotkey: "cell.run", hidden: status === "running" || status === "queued" || status === "disabled-transitively" || config.disabled, redundant: true, handle: () => runCell(), }, { icon: , label: "Refactor with AI", hidden: !aiEnabled, handle: () => { setAiCompletionCell((current) => current?.cellId === cellId ? null : { cellId }, ); }, hotkey: "cell.aiCompletion", }, { icon: , label: "Split", hotkey: "cell.splitCell", handle: () => splitCell({ cellId }), }, { icon: , label: "Format", hotkey: "cell.format", handle: () => { const editorView = getEditorView(); if (!editorView) { return; } formatEditorViews({ [cellId]: editorView }); }, }, { icon: config.hide_code ? ( ) : ( ), label: config.hide_code ? "Show code" : "Hide code", handle: toggleHideCode, hotkey: "cell.hideCode", }, { icon: config.disabled ? ( ) : ( ), label: config.disabled ? "Enable execution" : "Disable execution", handle: toggleDisabled, hidden: isSetupCell, }, ], // View as [ { icon: , label: "Convert to Markdown", hotkey: "cell.viewAsMarkdown", handle: async () => { const editorView = getEditorView(); if (!editorView) { return; } maybeAddMarimoImport({ autoInstantiate, createNewCell: createCell }); switchLanguage(editorView, { language: "markdown", keepCodeAsIs: false, }); // Code stays visible until the user blurs the cell if (!config.hide_code && MARKDOWN_INITIAL_HIDE_CODE) { await saveCellConfig({ configs: { [cellId]: { hide_code: MARKDOWN_INITIAL_HIDE_CODE } }, }); updateCellConfig({ cellId, config: { hide_code: MARKDOWN_INITIAL_HIDE_CODE }, }); markUntouched({ cellId }); } }, hidden: isSetupCell, }, // We need to check this here because the user may have toggled the language getCurrentLanguageAdapter(getEditorView()) === "sql" ? { icon: , label: "View as Python", hotkey: "cell.viewAsSQL", handle: () => { const editorView = getEditorView(); if (!editorView) { return; } toggleToLanguage(editorView, "python", { force: true }); }, hidden: isSetupCell, } : { icon: , label: "Convert to SQL", hotkey: "cell.viewAsSQL", handle: () => { const editorView = getEditorView(); if (!editorView) { return; } maybeAddMarimoImport({ autoInstantiate, createNewCell: createCell, }); switchLanguage(editorView, { language: "sql", keepCodeAsIs: false, }); }, hidden: isSetupCell, }, ], // Movement [ { icon: ( ), label: "Create cell above", hotkey: "cell.createAbove", handle: () => createCell({ cellId, before: true }), hidden: isSetupCell, redundant: true, }, { icon: ( ), label: "Create cell below", hotkey: "cell.createBelow", handle: () => createCell({ cellId, before: false }), redundant: true, }, { icon: , label: "Move cell up", hotkey: "cell.moveUp", handle: () => moveCell({ cellId, before: true }), hidden: isSetupCell, }, { icon: , label: "Move cell down", hotkey: "cell.moveDown", handle: () => moveCell({ cellId, before: false }), hidden: isSetupCell, }, { icon: , label: "Move cell left", hotkey: "cell.moveLeft", handle: () => moveCell({ cellId, direction: "left" }), hidden: appWidth !== "columns" || isSetupCell, }, { icon: , label: "Move cell right", hotkey: "cell.moveRight", handle: () => moveCell({ cellId, direction: "right" }), hidden: appWidth !== "columns" || isSetupCell, }, { icon: , label: "Send to top", hotkey: "cell.sendToTop", // When using the cell menu, likely the user doesn't want to scroll // and instead just wants to get the cell out of the way handle: () => sendToTop({ cellId, scroll: false }), hidden: isSetupCell, }, { icon: , label: "Send to bottom", hotkey: "cell.sendToBottom", // When using the cell menu, likely the user doesn't want to scroll // and instead just wants to get the cell out of the way handle: () => sendToBottom({ cellId, scroll: false }), hidden: isSetupCell, }, { icon: , label: "Break into new column", hotkey: "cell.addColumnBreakpoint", handle: () => addColumnBreakpoint({ cellId }), hidden: appWidth !== "columns" || isSetupCell, }, ], // Outputs [ { icon: , label: "Export output as PNG", hidden: !hasOutput, handle: () => downloadCellOutputAsImage(cellId, "result"), }, { icon: , label: "Clear output", hidden: !(hasOutput || hasConsoleOutput), handle: () => { clearCellOutput({ cellId }); }, }, ], // Link to cell [ { icon: , label: "Name", disableClick: true, handle: (evt) => { evt?.stopPropagation(); evt?.preventDefault(); }, handleHeadless: () => { openModal( Rename cell
{ if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); openModal(null); } }} onChange={(newName) => updateCellName({ cellId, name: newName }) } />
, ); }, rightElement: ( updateCellName({ cellId, name: newName })} onEnterKey={() => closePopover?.()} /> ), hidden: isSetupCell, }, { icon: , label: "Copy link to cell", disabled: !canLinkToCell(name), tooltip: canLinkToCell(name) ? undefined : "Only named cells can be linked to", handle: async () => { await copyToClipboard(createCellLink(name)); toast({ description: "Link copied to clipboard" }); }, }, ], // Delete [ { label: "Delete", hidden: !canDelete, variant: "danger", icon: , handle: () => { deleteCell({ cellId }); }, }, ], ]; // remove hidden return actions .map((group) => group.filter((action) => !action.hidden)) .filter((group) => group.length > 0); }