/* Copyright 2026 Marimo. All rights reserved. */ import { zodResolver } from "@hookform/resolvers/zod"; import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { merge } from "lodash-es"; import { AlertTriangleIcon, BrainIcon, CpuIcon, EditIcon, FlaskConicalIcon, FolderCog2, LayersIcon, MonitorIcon, } from "lucide-react"; import React, { useId, useRef } from "react"; import { useLocale } from "react-aria"; import { useForm } from "react-hook-form"; import type z from "zod"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { acceptCompletionOnEnterAtom } from "@/core/codemirror/completion/accept-on-enter-atom"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Kbd } from "@/components/ui/kbd"; import { NativeSelect } from "@/components/ui/native-select"; import { NumberField } from "@/components/ui/number-field"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { KEYMAP_PRESETS } from "@/core/codemirror/keymaps/keymaps"; import { capabilitiesAtom } from "@/core/config/capabilities"; import { useUserConfig } from "@/core/config/config"; import { PackageManagerNames, type UserConfig, UserConfigSchema, } from "@/core/config/config-schema"; import { getAppWidths } from "@/core/config/widths"; import { marimoVersionAtom } from "@/core/meta/state"; import { viewStateAtom } from "@/core/mode"; import { useRequestClient } from "@/core/network/requests"; import { isWasm } from "@/core/wasm/utils"; import { useDebouncedCallback } from "@/hooks/useDebounce"; import { Banner } from "@/plugins/impl/common/error-banner"; import { THEMES } from "@/theme/useTheme"; import { arrayToggle } from "@/utils/arrays"; import { cn } from "@/utils/cn"; import { autoPopulateModels } from "../ai/ai-utils"; import { keyboardShortcutsAtom } from "../editor/controls/keyboard-shortcuts"; import { Badge } from "../ui/badge"; import { ExternalLink } from "../ui/links"; import { Tooltip } from "../ui/tooltip"; import { AiConfig } from "./ai-config"; import { formItemClasses, SettingGroup } from "./common"; import { DataForm } from "./data-form"; import { applyManualInjections, getDirtyValues } from "./get-dirty-values"; import { IsOverridden } from "./is-overridden"; import { OptionalFeatures } from "./optional-features"; const categories = [ { id: "editor", label: "Editor", Icon: EditIcon, className: "bg-(--blue-4)", }, { id: "display", label: "Display", Icon: MonitorIcon, className: "bg-(--grass-4)", }, { id: "packageManagementAndData", label: "Packages & Data", Icon: LayersIcon, className: "bg-(--red-4)", }, { id: "runtime", label: "Runtime", Icon: CpuIcon, className: "bg-(--amber-4)", }, { id: "ai", label: "AI", Icon: BrainIcon, className: "bg-[linear-gradient(45deg,var(--purple-5),var(--cyan-5))]", }, { id: "optionalDeps", label: "Optional Dependencies", Icon: FolderCog2, className: "bg-(--orange-4)", }, { id: "labs", label: "Labs", Icon: FlaskConicalIcon, className: "bg-(--slate-4)", }, ] as const; export type SettingCategoryId = (typeof categories)[number]["id"]; export const activeUserConfigCategoryAtom = atom( categories[0].id, ); const FORM_DEBOUNCE = 100; // ms; const LOCALE_SYSTEM_VALUE = "__system__"; export const UserConfigForm: React.FC = () => { const [config, setConfig] = useUserConfig(); const [acceptOnEnter, setAcceptOnEnter] = useAtom( acceptCompletionOnEnterAtom, ); const formElement = useRef(null); const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom); const [activeCategory, setActiveCategory] = useAtom( activeUserConfigCategoryAtom, ); let capabilities = useAtomValue(capabilitiesAtom); const isHome = useAtomValue(viewStateAtom).mode === "home"; // The home page does not fetch kernel capabilities, so we just turn them all on if (isHome) { capabilities = { terminal: true, pylsp: true, ty: true, basedpyright: true, pyrefly: true, }; } const marimoVersion = useAtomValue(marimoVersionAtom); const { locale } = useLocale(); const { saveUserConfig } = useRequestClient(); // Create form const form = useForm({ resolver: zodResolver( UserConfigSchema as unknown as z.ZodType, ), defaultValues: config, }); const setAiModels = ( values: UserConfig["ai"], dirtyAiConfig: UserConfig["ai"], ) => { const { chatModel, editModel } = autoPopulateModels(values); if (chatModel || editModel) { dirtyAiConfig = { ...dirtyAiConfig, models: { ...dirtyAiConfig?.models, ...(chatModel && { chat_model: chatModel }), ...(editModel && { edit_model: editModel }), }, } as typeof dirtyAiConfig; if (chatModel) { form.setValue("ai.models.chat_model", chatModel); } if (editModel) { form.setValue("ai.models.edit_model", editModel); } } return dirtyAiConfig; }; const onSubmitNotDebounced = async (values: UserConfig) => { // Only send values that were actually changed to avoid // overwriting backend values the form doesn't manage const dirtyValues = getDirtyValues(values, form.formState.dirtyFields); applyManualInjections({ values, dirtyValues, touchedFields: form.formState.touchedFields, }); if (Object.keys(dirtyValues).length === 0) { return; // Nothing changed } // Auto-populate AI models when credentials are set, makes it easier to get started if (dirtyValues.ai) { dirtyValues.ai = setAiModels(values.ai, dirtyValues.ai); } await saveUserConfig({ config: dirtyValues }); // Only apply the changed keys; this avoids stale request responses // overwriting newer config changes. setConfig((prev) => merge({}, prev, dirtyValues)); }; const onSubmit = useDebouncedCallback(onSubmitNotDebounced, FORM_DEBOUNCE); const isWasmRuntime = isWasm(); const htmlCheckboxId = useId(); const ipynbCheckboxId = useId(); const renderBody = () => { switch (activeCategory) { case "editor": return ( <> ( Autosave enabled { field.onChange(checked ? "after_delay" : "off"); }} /> )} /> ( Autosave delay (seconds) { field.onChange(value * 1000); if (!Number.isNaN(value)) { onSubmit(form.getValues()); } }} /> )} /> {/* auto_download is a runtime setting in the backend, but it makes * more sense as an autosave setting. */} (
Save cell outputs as
{ const currentValue = Array.isArray(field.value) ? field.value : []; field.onChange( arrayToggle(currentValue, "html"), ); }} /> HTML
{ const currentValue = Array.isArray(field.value) ? field.value : []; field.onChange( arrayToggle(currentValue, "ipynb"), ); }} /> IPYNB
When enabled, marimo will periodically save notebooks in your selected formats (HTML, IPYNB) to a folder named{" "} __marimo__ next to your notebook file.
)} />
( Format on save { field.onChange(checked); }} /> )} /> (
Line length { // Ignore NaN field.onChange(value); if (!Number.isNaN(value)) { onSubmit(form.getValues()); } }} /> Maximum line length when formatting code.
)} />
(
Autocomplete { field.onChange(Boolean(checked)); }} /> When unchecked, code completion is still available through a hotkey.
)} />
Accept suggestion on Enter setAcceptOnEnter(Boolean(checked)) } /> When unchecked, pressing Enter inserts a new line instead of accepting an autocomplete suggestion. Use Tab to accept suggestions.
(
Signature hints { field.onChange(Boolean(checked)); }} /> Display signature hints while typing within function calls.
)} />
See the{" "} docs {" "} for more information about language server support. Note: When using multiple language servers, different features may conflict. (
Beta Python Language Server ( docs ) { field.onChange(Boolean(checked)); }} /> {field.value && !capabilities.pylsp && ( The Python Language Server is not available in your current environment. Please install{" "} python-lsp-server in your environment. )}
)} /> (
Beta basedpyright ( docs ) { field.onChange(Boolean(checked)); }} /> {field.value && !capabilities.basedpyright && ( basedpyright is not available in your current environment. Please install{" "} basedpyright in your environment. )}
)} /> (
Beta Pyrefly ( docs ) { field.onChange(Boolean(checked)); }} /> {field.value && !capabilities.pyrefly && ( Pyrefly is not available in your current environment. Please install pyrefly in your environment. )}
)} /> (
Beta ty ( docs ) { field.onChange(Boolean(checked)); }} /> {field.value && !capabilities.ty && ( ty is not available in your current environment. Please install ty in your environment. )}
)} /> ( Beta Diagnostics { field.onChange(Boolean(checked)); }} /> )} />
(
Keymap field.onChange(e.target.value)} value={field.value} disabled={field.disabled} className="inline-flex mr-2" > {KEYMAP_PRESETS.map((option) => ( ))}
)} /> (
Destructive delete { field.onChange(Boolean(checked)); }} /> Allow deleting non-empty cells Use with caution: Deleting cells with code can lose work and computed results since variables are removed from memory.
} >
)} />
); case "display": return ( <> (
Default width field.onChange(e.target.value)} value={field.value} disabled={field.disabled} className="inline-flex mr-2" > {getAppWidths().map((option) => ( ))} The default app width for new notebooks; overridden by "width" in the application config.
)} /> (
Theme field.onChange(e.target.value)} value={field.value} disabled={field.disabled} className="inline-flex mr-2" > {THEMES.map((option) => ( ))} This theme will be applied to the user's configuration; it does not affect theme when sharing the notebook.
)} /> ( Code editor font size (px) { field.onChange(value); onSubmit(form.getValues()); }} /> )} /> (
Locale { if (e.target.value === LOCALE_SYSTEM_VALUE) { field.onChange(undefined); } else { field.onChange(e.target.value); } }} value={field.value || LOCALE_SYSTEM_VALUE} disabled={field.disabled} className="inline-flex mr-2" > {navigator.languages.map((option) => ( ))} The locale to use for the notebook. If your desired locale is not listed, you can change it manually via{" "} marimo config show.
)} /> (
Reference highlighting Visually emphasizes variables in a cell that are defined elsewhere in the notebook.
)} />
(
Cell output area field.onChange(e.target.value)} value={field.value} disabled={field.disabled} className="inline-flex mr-2" > {["above", "below"].map((option) => ( ))} Where to display cell's output.
)} />
); case "packageManagementAndData": return ( <> (
Manager field.onChange(e.target.value)} value={field.value} disabled={field.disabled} className="inline-flex mr-2" > {PackageManagerNames.map((option) => ( ))} When marimo comes across a module that is not installed, you will be prompted to install it using your preferred package manager. Learn more in the{" "} docs .

Running marimo in a{" "} sandboxed environment {" "} is only supported by uv
)} />
); case "runtime": return ( (
Autorun on startup Whether to automatically run all cells on startup.
)} /> (
On cell change field.onChange(e.target.value)} value={field.value} className="inline-flex mr-2" > {["lazy", "autorun"].map((option) => ( ))} Whether marimo should automatically run cells or just mark them as stale. If "autorun", marimo will automatically run affected cells when a cell is run or a UI element is interacted with; if "lazy", marimo will mark affected cells as stale but won't re-run them.
)} /> (
On module change field.onChange(e.target.value)} value={field.value} disabled={isWasmRuntime} className="inline-flex mr-2" > {["off", "lazy", "autorun"].map((option) => ( ))} Whether marimo should automatically reload modules before executing cells. If "lazy", marimo will mark cells affected by module modifications as stale; if "autorun", affected cells will be automatically re-run.
)} /> (
Autorun Unit Tests Enable reactive pytest tests in notebook. When a cell contains only test functions (test_*) and classes (Test_*), marimo will automatically run them with pytest (requires notebook restart). {" "}
)} /> Learn more in the{" "} docs .
); case "ai": return ; case "optionalDeps": return ; case "labs": return (

⚠️ These features are experimental and may require restarting your notebook to take effect.

(
Real-Time Collaboration Enable experimental real-time collaboration. This change requires a page refresh to take effect.
)} /> (
External Agents Enable experimental external agents such as Claude Code and Gemini CLI. Learn more in the{" "} docs .
)} />
); } }; const configMessage = (

User configuration is stored in marimo.toml
Run marimo config show in your terminal to show your current configuration and file location.

); return (
setActiveCategory(value as SettingCategoryId) } orientation="vertical" className="w-1/3 border-r h-full overflow-auto p-3" > {categories.map((category) => (
{category.label}
))}
Version: {marimoVersion} Locale: {locale}
{!isWasm() && configMessage}
{renderBody()}
); };