/* Copyright 2026 Marimo. All rights reserved. */ import { useAtom, useAtomValue, useSetAtom } from "jotai"; import type { RefObject } from "react"; import { BookMarkedIcon, CheckIcon, ChevronDownCircleIcon, ChevronRightCircleIcon, ClipboardCopyIcon, CodeIcon, CommandIcon, DatabaseIcon, DiamondPlusIcon, DownloadIcon, EditIcon, ExternalLinkIcon, EyeIcon, EyeOffIcon, FastForwardIcon, FileIcon, Files, FileTextIcon, FolderDownIcon, GlobeIcon, HardDrive, Home, ImageIcon, KeyboardIcon, LayoutTemplateIcon, LinkIcon, MessageCircleQuestionIcon, MessagesSquareIcon, NotebookIcon, PanelLeftIcon, PowerSquareIcon, PresentationIcon, SettingsIcon, Share2Icon, SparklesIcon, Undo2Icon, XCircleIcon, ZapIcon, } from "lucide-react"; import { settingDialogAtom, useOpenSettingsToTab, } from "@/components/app-config/state"; import { FeedbackModal } from "@/components/editor/chrome/components/feedback-button"; import { MarkdownIcon } from "@/components/editor/cell/code/icons"; import { GitHubIcon } from "@/components/icons/github"; import { MarimoPlusIcon } from "@/components/icons/marimo-icons"; import { YouTubeIcon } from "@/components/icons/youtube"; import { useImperativeModal } from "@/components/modal/ImperativeModal"; import { PairWithAgentModal } from "@/components/editor/actions/pair-with-agent-modal"; import { ShareStaticNotebookModal } from "@/components/static-html/share-modal"; import { toast } from "@/components/ui/use-toast"; import { canUndoDeletesAtom, getNotebook, hasDisabledCellsAtom, undoLabelAtom, useCellActions, } from "@/core/cells/cells"; import { disabledCellIds } from "@/core/cells/utils"; import { capabilitiesAtom } from "@/core/config/capabilities"; import { aiEnabledAtom, useResolvedMarimoConfig } from "@/core/config/config"; import { Constants } from "@/core/constants"; import { useLayoutActions, useLayoutState } from "@/core/layout/state"; import { useTogglePresenting } from "@/core/layout/useTogglePresenting"; import { kioskModeAtom, viewStateAtom } from "@/core/mode"; import { useRequestClient } from "@/core/network/requests"; import { useFilename } from "@/core/saving/filename"; import { createShareableLink } from "@/core/wasm/share"; import { isWasm } from "@/core/wasm/utils"; import { useDetectedDataSources } from "@/hooks/useDataSourceDiscovery"; import { copyToClipboard } from "@/utils/copy"; import { Objects } from "@/utils/objects"; import { Strings } from "@/utils/strings"; import { newNotebookURL } from "@/utils/urls"; import { useRunAllCells } from "../cell/useRunCells"; import { useChromeActions, useChromeState } from "../chrome/state"; import { isPanelHidden, PANELS } from "../chrome/types"; import { AddConnectionDialogContent } from "../connections/add-connection-dialog"; import { useAddDetectedDataSource } from "../connections/components"; import { DATABASE_CONNECTION_KEYWORDS } from "../connections/database/add-database-form"; import { STORAGE_CONNECTION_KEYWORDS } from "../connections/storage/add-storage-form"; import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts"; import { commandPaletteAtom } from "../controls/state"; import { displayLayoutName, getLayoutIcon } from "../renderers/layout-select"; import { LAYOUT_TYPES } from "../renderers/types"; import { ExportDialog } from "./export-dialog/export-dialog"; import { applyExportOptionOverrides, exportOptionsAtom, type ExportFormat, type ExportOptionOverrides, } from "./export-dialog/state"; import type { ActionButton } from "./types"; import { useCopyNotebook } from "./useCopyNotebook"; import { useRestartKernel } from "./useRestartKernel"; import { useSetCodeVisibility } from "./useSetCodeVisibility"; const NOOP_HANDLER = (event?: Event) => { event?.preventDefault(); event?.stopPropagation(); }; export function useNotebookActions({ exportDialogReturnFocusRef, }: { exportDialogReturnFocusRef?: RefObject; } = {}) { const filename = useFilename(); const { openModal, closeModal } = useImperativeModal(); const { toggleApplication } = useChromeActions(); const { selectedPanel } = useChromeState(); const [viewState] = useAtom(viewStateAtom); const kioskMode = useAtomValue(kioskModeAtom); const setCodeVisibility = useSetCodeVisibility(); const [resolvedConfig] = useResolvedMarimoConfig(); const capabilities = useAtomValue(capabilitiesAtom); const aiEnabled = useAtomValue(aiEnabledAtom); const { updateCellConfig, undoDeleteCell, clearAllCellOutputs, addSetupCellIfDoesntExist, collapseAllCells, expandAllCells, } = useCellActions(); const restartKernel = useRestartKernel(); const runAllCells = useRunAllCells(); const copyNotebook = useCopyNotebook(filename); const setCommandPaletteOpen = useSetAtom(commandPaletteAtom); const setSettingsDialogOpen = useSetAtom(settingDialogAtom); const { handleClick: openSettings } = useOpenSettingsToTab(); const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom); const setExportOptions = useSetAtom(exportOptionsAtom); const { readCode, saveCellConfig } = useRequestClient(); const detectedDatabaseSources = useDetectedDataSources("database"); const detectedStorageSources = useDetectedDataSources("storage"); const addDetectedDataSource = useAddDetectedDataSource(); const hasDisabledCells = useAtomValue(hasDisabledCellsAtom); const canUndoDeletes = useAtomValue(canUndoDeletesAtom); const undoLabel = useAtomValue(undoLabelAtom); const { selectedLayout } = useLayoutState(); const { setLayoutView } = useLayoutActions(); const togglePresenting = useTogglePresenting(); // Fallback: if sharing is undefined, all options are enabled by default const sharingHtmlEnabled = resolvedConfig.sharing?.html ?? true; const sharingWasmEnabled = resolvedConfig.sharing?.wasm ?? true; const sharingMolabEnabled = resolvedConfig.sharing?.molab ?? true; const isSlidesLayout = selectedLayout === "slides"; const databaseConnectionKeywords = [ "db", "sql", ...DATABASE_CONNECTION_KEYWORDS, ]; const storageConnectionKeywords = [ "bucket", "object storage", ...STORAGE_CONNECTION_KEYWORDS, ]; const renderCheckboxElement = (checked: boolean) => (
{checked && }
); const openExportDialog = ( initialFormat?: ExportFormat, optionOverrides?: ExportOptionOverrides, ) => { if (optionOverrides) { setExportOptions((current) => applyExportOptionOverrides(current, optionOverrides), ); } openModal( , ); }; const actions: ActionButton[] = [ { icon: , label: "Export…", additionalKeywords: [ "download", "html", "markdown", "ipynb", "pdf", "script", "png", ], handle: () => openExportDialog(), }, { icon: , label: "Download", redundant: true, handle: NOOP_HANDLER, dropdown: [ { icon: , label: "Download as HTML", handle: () => openExportDialog("html", { html: { includeCode: true } }), }, { icon: , label: "Download as HTML (exclude code)", handle: () => openExportDialog("html", { html: { includeCode: false } }), }, { icon: ( ), label: "Download as Markdown", handle: () => openExportDialog("markdown"), }, { icon: , label: "Download as ipynb", handle: () => openExportDialog("ipynb"), }, { icon: , label: "Download notebook source", handle: () => openExportDialog("script", { script: { type: "source" } }), }, { icon: , label: "Download flat script", handle: () => openExportDialog("script", { script: { type: "flat" } }), }, { divider: true, icon: , label: "Download as PNG", handle: () => openExportDialog("png"), }, isSlidesLayout ? { divider: true, icon: , label: "Download as PDF", handle: NOOP_HANDLER, dropdown: [ { icon: , label: "Document Layout", handle: () => openExportDialog("pdf", { pdf: { preset: "document" }, }), }, { icon: , label: "Slides Layout", hidden: isWasm(), rightElement: ( Recommended ), handle: () => openExportDialog("pdf", { pdf: { preset: "slides" }, }), }, ], } : { divider: true, icon: , label: "Download as PDF", handle: () => openExportDialog("pdf", { pdf: { preset: "document" }, }), }, ], }, { icon: , label: "Connect your agent", hidden: isWasm(), handle: async () => { openModal(); }, additionalKeywords: ["connect"], }, { icon: , label: "Share", handle: NOOP_HANDLER, hidden: !sharingHtmlEnabled && !sharingWasmEnabled && !sharingMolabEnabled, dropdown: [ { icon: , label: "Publish HTML to web", hidden: !sharingHtmlEnabled, handle: async () => { openModal(); }, }, { icon: , label: "Create WebAssembly link", hidden: !sharingWasmEnabled, handle: async () => { const code = await readCode(); const url = createShareableLink({ code: code.contents }); await copyToClipboard(url); toast({ title: "Copied", description: "Link copied to clipboard.", }); }, }, { icon: , label: "Create molab notebook", hidden: !sharingMolabEnabled, handle: async () => { const code = await readCode(); const url = createShareableLink({ code: code.contents, baseUrl: `${Constants.molab}/new`, }); window.open(url, "_blank"); }, }, ], }, { icon: , label: "Helper panel", redundant: true, handle: NOOP_HANDLER, dropdown: PANELS.flatMap((panel) => { // Still show the AI panel in the command palette so users can try AI // features. When AI is disabled, open settings instead of the panel. const openAiSettingsWhenDisabled = panel.type === "ai" && !aiEnabled; if ( isPanelHidden({ panel, capabilities, aiEnabled }) && !openAiSettingsWhenDisabled ) { return []; } const { type: id, Icon, additionalKeywords } = panel; return { label: Strings.startCase(id), rightElement: renderCheckboxElement(selectedPanel === id), icon: , handle: () => { if (openAiSettingsWhenDisabled) { openSettings("ai", "ai-features"); return; } toggleApplication(id); }, additionalKeywords, }; }), }, { icon: , label: "Present as", handle: NOOP_HANDLER, dropdown: [ { icon: viewState.mode === "present" ? ( ) : ( ), label: "Toggle app view", hotkey: "global.hideCode", handle: () => { togglePresenting(); }, }, ...LAYOUT_TYPES.map((type, idx) => { const Icon = getLayoutIcon(type); return { divider: idx === 0, label: displayLayoutName(type), icon: , rightElement: (
{selectedLayout === type && }
), handle: () => { setLayoutView(type); // Toggle if it's not in present mode if (viewState.mode === "edit") { togglePresenting(); } }, }; }), ], }, { icon: , label: "Duplicate notebook", hidden: !filename || isWasm(), handle: copyNotebook, }, { icon: , label: "Copy code to clipboard", hidden: !filename, handle: async () => { const code = await readCode(); await copyToClipboard(code.contents); toast({ title: "Copied", description: "Code copied to clipboard.", }); }, }, { icon: , label: "Enable all cells", hidden: !hasDisabledCells || kioskMode, handle: async () => { const notebook = getNotebook(); const ids = disabledCellIds(notebook); const newConfigs = Objects.fromEntries( ids.map((cellId) => [cellId, { disabled: false }]), ); // send to BE await saveCellConfig({ configs: newConfigs }); // update on FE for (const cellId of ids) { updateCellConfig({ cellId, config: { disabled: false } }); } }, }, { divider: true, icon: , label: "Add setup cell", handle: () => { addSetupCellIfDoesntExist({}); }, }, { icon: , label: "Add database connection", additionalKeywords: databaseConnectionKeywords, handle: () => { openModal(); }, dropdown: detectedDatabaseSources.length === 0 ? undefined : [ ...detectedDatabaseSources.map((source) => ({ icon: , label: `Add ${source.displayName}`, description: "Detected in your environment", handle: () => addDetectedDataSource(source), })), { divider: true, icon: , label: "Browse all connections", additionalKeywords: databaseConnectionKeywords, handle: () => { openModal( , ); }, }, ], }, { icon: , label: "Add remote storage", additionalKeywords: storageConnectionKeywords, handle: () => { openModal( , ); }, dropdown: detectedStorageSources.length === 0 ? undefined : [ ...detectedStorageSources.map((source) => ({ icon: , label: `Add ${source.displayName}`, description: "Detected in your environment", handle: () => addDetectedDataSource(source), })), { divider: true, icon: , label: "Browse all connections", additionalKeywords: storageConnectionKeywords, handle: () => { openModal( , ); }, }, ], }, { icon: , label: undoLabel, hidden: !canUndoDeletes || kioskMode, handle: () => { undoDeleteCell(); }, }, { icon: , label: "Restart kernel", variant: "danger", handle: restartKernel, additionalKeywords: ["reset", "reload", "restart"], }, { icon: , label: "Re-run all cells", redundant: true, hotkey: "global.runAll", handle: async () => { runAllCells(); }, }, { icon: , label: "Clear all outputs", redundant: true, handle: () => { clearAllCellOutputs(); }, }, { icon: , label: "Show all code", hotkey: "global.showAllCode", handle: () => setCodeVisibility(false, "code"), redundant: true, }, { icon: , label: "Hide all code", hotkey: "global.hideAllCode", handle: () => setCodeVisibility(true, "code"), redundant: true, }, { icon: , label: "Show all markdown code", hotkey: "global.showAllMarkdownCode", handle: () => setCodeVisibility(false, "markdown"), redundant: true, }, { icon: , label: "Hide all markdown code", hotkey: "global.hideAllMarkdownCode", handle: () => setCodeVisibility(true, "markdown"), redundant: true, }, { icon: , label: "Collapse all sections", hotkey: "global.collapseAllSections", handle: collapseAllCells, redundant: true, }, { icon: , label: "Expand all sections", hotkey: "global.expandAllSections", handle: expandAllCells, redundant: true, }, { divider: true, icon: , label: "Command palette", hotkey: "global.commandPalette", handle: () => setCommandPaletteOpen((open) => !open), }, { icon: , label: "Keyboard shortcuts", hotkey: "global.showHelp", handle: () => setKeyboardShortcutsOpen((open) => !open), }, { icon: , label: "User settings", handle: () => setSettingsDialogOpen((open) => !open), redundant: true, additionalKeywords: ["preferences", "options", "configuration"], }, { icon: , label: "Report an issue", additionalKeywords: ["feedback", "bug", "issue", "report", "diagnostics"], handle: () => openModal(), }, { icon: , label: "Resources", handle: NOOP_HANDLER, dropdown: [ { icon: , label: "Documentation", handle: () => { window.open(Constants.docsPage, "_blank"); }, }, { icon: , label: "GitHub", handle: () => { window.open(Constants.githubPage, "_blank"); }, }, { icon: , label: "Discord Community", handle: () => { window.open(Constants.discordLink, "_blank"); }, }, { icon: , label: "YouTube", handle: () => { window.open(Constants.youtube, "_blank"); }, }, { icon: , label: "Changelog", handle: () => { window.open(Constants.releasesPage, "_blank"); }, }, ], }, { divider: true, icon: , label: "Open home", // If file is in the url, then we ran `marimo edit` // without a specific file hidden: !location.search.includes("file"), handle: () => { const withoutSearch = document.baseURI.split("?")[0]; window.open(withoutSearch, "_blank", "noopener"); }, }, { icon: , label: "New notebook", // If file is in the url, then we ran `marimo edit` // without a specific file hidden: !location.search.includes("file"), handle: () => { const url = newNotebookURL(); window.open(url, "_blank"); }, }, ]; return actions .filter((a) => !a.hidden) .map((action) => { if (action.dropdown) { return { ...action, dropdown: action.dropdown.filter((item) => !item.hidden), }; } return action; }); }