/* Copyright 2026 Marimo. All rights reserved. */ import { useAtom } from "jotai"; import { BotIcon, BrainIcon, ChevronRightIcon, InfoIcon, PlusIcon, Trash2Icon, } from "lucide-react"; import React, { useId, useMemo, useState } from "react"; import { Button as AriaButton, Tree, TreeItem, TreeItemContent, } from "react-aria-components"; import type { FieldPath, UseFormReturn } from "react-hook-form"; import { useWatch } from "react-hook-form"; import useEvent from "react-use-event-hook"; import { FormControl, FormDescription, FormErrorsBanner, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Kbd } from "@/components/ui/kbd"; import { NativeSelect } from "@/components/ui/native-select"; import { Textarea } from "@/components/ui/textarea"; import type { SupportedRole } from "@/core/ai/config"; import { AiModelId, KNOWN_PROVIDERS, type KnownProviderId, type ProviderId, type QualifiedModelId, type ShortModelId, } from "@/core/ai/ids/ids"; import { type AiModel, AiModelRegistry } from "@/core/ai/model-registry"; import { CopilotConfig } from "@/core/codemirror/copilot/copilot-config"; import { DEFAULT_AI_MODEL, type UserConfig } from "@/core/config/config-schema"; import { isWasm } from "@/core/wasm/utils"; import { cn } from "@/utils/cn"; import { Events } from "@/utils/events"; import { Strings } from "@/utils/strings"; import { AIModelDropdown, getProviderLabel } from "../ai/ai-model-dropdown"; import { AiProviderIcon, type AiProviderIconProps, } from "../ai/ai-provider-icon"; import { getTagColour } from "../ai/display-helpers"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "../ui/accordion"; import { Alert, AlertDescription } from "../ui/alert"; import { Button } from "../ui/button"; import { Checkbox } from "../ui/checkbox"; import { DropdownMenuSeparator } from "../ui/dropdown-menu"; import { Label } from "../ui/label"; import { ExternalLink } from "../ui/links"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, } from "../ui/select"; import { Switch } from "../ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Tooltip } from "../ui/tooltip"; import { formItemClasses, SettingSubtitle } from "./common"; import { AWS_REGIONS } from "./constants"; import { IncorrectModelId } from "./incorrect-model-id"; import { IsOverridden } from "./is-overridden"; import { MCPConfig } from "./mcp-config"; import { aiSettingsSubTabAtom } from "./state"; interface AiConfigProps { form: UseFormReturn; config: UserConfig; onSubmit: (values: UserConfig) => void; } interface AiProviderTitleProps { provider?: AiProviderIconProps["provider"]; children: React.ReactNode; } interface CustomProviderConfig { api_key?: string; base_url?: string; } export const AiProviderTitle: React.FC = ({ provider, children, }) => { return (
{provider && } {children}
); }; interface ApiKeyProps { form: UseFormReturn; config: UserConfig; name: FieldPath; placeholder: string; testId: string; description?: React.ReactNode; onChange?: (value: string) => void; } export const ApiKey: React.FC = ({ form, config, name, placeholder, testId, description, onChange, }) => { return ( (
API Key { const value = e.target.value; if (!value.includes("*")) { field.onChange(value); onChange?.(value); } }} /> {description && {description}}
)} /> ); }; interface BaseUrlProps { form: UseFormReturn; config: UserConfig; name: FieldPath; placeholder: string; testId: string; description?: React.ReactNode; disabled?: boolean; onChange?: (value: string) => void; } function asStringOrEmpty(value: T): string { if (value == null) { return ""; } if (typeof value === "string") { return value; } return String(value); } export const BaseUrl: React.FC = ({ form, config, name, placeholder, testId, description, disabled = false, onChange, }) => { return ( (
Base URL { field.onChange(e.target.value); onChange?.(e.target.value); }} /> {description && {description}}
)} /> ); }; interface ModelSelectorProps { form: UseFormReturn; config: UserConfig; name: FieldPath; placeholder: string; description?: React.ReactNode; label: string; forRole: SupportedRole; onSubmit: (values: UserConfig) => void; } export const ModelSelector: React.FC = ({ form, config, name, placeholder, description, label, forRole, onSubmit, }) => { return ( { const value = asStringOrEmpty(field.value); const selectModel = (modelId: QualifiedModelId) => { field.onChange(modelId); // Usually not needed, but a hack to force form values to be updated onSubmit(form.getValues()); }; const renderFormItem = () => ( {label}

Enter a custom model

{value && ( )}
} forRole={forRole} />
); return (
{renderFormItem()} {description && {description}}
); }} /> ); }; interface ProviderSelectProps { form: UseFormReturn; config: UserConfig; name: FieldPath; options: string[]; testId: string; disabled?: boolean; } export const ProviderSelect: React.FC = ({ form, config, name, options, testId, disabled = false, }) => { return ( (
Provider { if (e.target.value === "none") { field.onChange(false); } else { field.onChange(e.target.value); } }} value={asStringOrEmpty( field.value === true ? "github" : field.value === false ? "none" : field.value, )} disabled={disabled} className="inline-flex mr-2" > {options.map((option) => ( ))}
)} /> ); }; const renderCopilotProvider = ({ form, config, onSubmit, }: { form: UseFormReturn; config: UserConfig; onSubmit: (values: UserConfig) => void; }) => { const copilot = form.getValues("completion.copilot"); if (copilot === false) { return null; } if (copilot === "codeium") { return ( <>

To get a Windsurf API key, follow{" "} these instructions .

); } if (copilot === "github") { return ; } if (copilot === "custom") { return ( ); } }; const SettingGroup = ({ children, className, }: { children: React.ReactNode; className?: string; }) => { return (
{children}
); }; interface ModelListItemProps { qualifiedId: QualifiedModelId; model: AiModel; isEnabled: boolean; onToggle: (modelId: QualifiedModelId) => void; onDelete: (modelId: QualifiedModelId) => void; } const ModelListItem: React.FC = ({ qualifiedId, model, isEnabled, onToggle, onDelete, }) => { const handleToggle = () => { onToggle(qualifiedId); }; const handleDelete = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); onDelete(qualifiedId); }; return (
{model.custom && ( )}
); }; const ModelInfoCard = ({ model }: { model: AiModel }) => { return (

{model.name}

{model.custom && } {model.thinking && (
Reasoning
)}
{model.description && !model.custom && (

{model.description}

)}
); }; export const AiCodeCompletionConfig: React.FC = ({ form, config, onSubmit, }) => { return ( Code Completion

Choose GitHub Copilot, Codeium, or a custom provider (such as Ollama) to enable AI-powered code completion.

{renderCopilotProvider({ form, config, onSubmit })}
); }; const AccordionFormItem = ({ title, triggerClassName, provider, children, isConfigured, value, }: { title: string; triggerClassName?: string; provider: AiProviderIconProps["provider"]; children: React.ReactNode; isConfigured: boolean; /** Custom value for the accordion item. Defaults to provider. */ value?: string; }) => { return ( {title} {isConfigured && ( Configured )} {children} ); }; export const CustomProvidersConfig: React.FC = ({ form, config, onSubmit, }) => { const [isAddingProvider, setIsAddingProvider] = useState(false); const [newProviderName, setNewProviderName] = useState(""); const [newProviderApiKey, setNewProviderApiKey] = useState(""); const [newProviderBaseUrl, setNewProviderBaseUrl] = useState(""); const providerNameInputId = useId(); const apiKeyInputId = useId(); const baseUrlInputId = useId(); const normalizedName = newProviderName.toLowerCase().replaceAll(/\s+/g, "_"); const customProviders = form.watch("ai.custom_providers"); const isDuplicate = KNOWN_PROVIDERS.includes(normalizedName as KnownProviderId) || (customProviders && Object.keys(customProviders).includes(normalizedName)); const hasInvalidChars = normalizedName.includes("."); const hasValidValues = normalizedName.trim() && newProviderBaseUrl.trim() && !isDuplicate && !hasInvalidChars; const resetForm = () => { setNewProviderName(""); setNewProviderApiKey(""); setNewProviderBaseUrl(""); setIsAddingProvider(false); }; return ( { const customProviders = (field.value || {}) as Record< string, CustomProviderConfig >; const customProviderEntries = Object.entries(customProviders); const addProvider = () => { if (!hasValidValues) { return; } field.onChange({ ...customProviders, [normalizedName]: { api_key: newProviderApiKey || undefined, base_url: newProviderBaseUrl, }, }); onSubmit(form.getValues()); resetForm(); }; const removeProvider = (providerName: string) => { const { [providerName]: _, ...rest } = customProviders; // Reset to clear nested dirty state, then set new value form.resetField("ai.custom_providers"); form.setValue("ai.custom_providers", rest, { shouldDirty: true }); onSubmit(form.getValues()); }; const providerForm = (
setNewProviderName(e.target.value)} /> {isDuplicate && (

A provider with this name already exists.

)} {hasInvalidChars && (

Provider names cannot contain '.' characters.

)} {newProviderName && !hasInvalidChars && (

Use models with prefix:{" "} {normalizedName}/

)}
setNewProviderBaseUrl(e.target.value)} />
setNewProviderApiKey(e.target.value)} />
); // Update a provider field by updating the entire custom_providers object. // As this config will be replaced, it needs to be sent in its entirety. const updateProviderField = (opts: { providerName: string; fieldName: keyof CustomProviderConfig; value: string; }) => { field.onChange({ ...customProviders, [opts.providerName]: { ...customProviders[opts.providerName], [opts.fieldName]: opts.value || undefined, }, }); }; const renderAccordionItem = ({ providerName, providerConfig, onRemove, }: { providerName: string; providerConfig: CustomProviderConfig; onRemove: (name: string) => void; }) => { const displayName = Strings.startCase(providerName); const isConfigured = !!providerConfig.api_key || !!providerConfig.base_url; return ( } placeholder="sk-..." testId={`custom-provider-${providerName}-api-key`} onChange={(value) => updateProviderField({ providerName, fieldName: "api_key", value, }) } /> } placeholder="https://api.example.com/v1" testId={`custom-provider-${providerName}-base-url`} onChange={(value) => updateProviderField({ providerName, fieldName: "base_url", value, }) } /> ); }; return ( Custom Providers

Add your own OpenAI-compatible provider. Once added, you can configure models in the AI Models tab.

{customProviderEntries.length > 0 && ( {customProviderEntries.map(([name, providerConfig]) => renderAccordionItem({ providerName: name, providerConfig, onRemove: removeProvider, }), )} )} {isAddingProvider ? ( providerForm ) : ( )}
); }} /> ); }; export const AiProvidersConfig: React.FC = ({ form, config, onSubmit, }) => { const isWasmRuntime = isWasm(); const hasValue = (name: FieldPath) => { return !!form.getValues(name); }; return (

Add your API keys below or to marimo.toml{" "} to set up a provider for the Code Completion and Assistant features; see{" "} docs {" "} for more info.

Your OpenAI API key from{" "} platform.openai.com . } /> Your Anthropic API key from{" "} console.anthropic.com . } /> Your Google AI API key from{" "} aistudio.google.com . } /> Free tier models have low token limits which can cause errors with larger prompts.{" "} Learn more Your GitHub API token from{" "} gh auth token. } /> Your OpenRouter API key from {""} openrouter.ai . } /> Your Weights & Biases API key from{" "} wandb.ai . } /> Your Azure API key from{" "} portal.azure.com . } />

To use AWS Bedrock, you need to configure AWS credentials and region. See the{" "} documentation {" "} for more details.

(
AWS Region field.onChange(e.target.value)} value={ typeof field.value === "string" ? field.value : "us-east-1" } disabled={field.disabled} className="inline-flex mr-2" > {AWS_REGIONS.map((option) => ( ))} The AWS region where Bedrock service is available.
)} /> (
AWS Profile Name (Optional) The AWS profile name from your ~/.aws/credentials file. Leave blank to use your default AWS credentials.
)} />

Consider using Custom Providers instead, which allows you to add multiple providers with distinct names.

API key for any OpenAI-compatible provider (e.g., Together, Groq, Mistral, Perplexity, etc). } /> Base URL for your OpenAI-compatible provider.} />
); }; export const AiAssistConfig: React.FC = ({ form, config, onSubmit, }) => { return ( AI Assistant (
AI Edit Tooltip Enable "Edit with AI" tooltip when selecting code.
)} /> Model to use for chat conversations in the Chat panel. } forRole="chat" onSubmit={onSubmit} /> Model to use for code editing with the{" "} Generate with AI button. } forRole="edit" onSubmit={onSubmit} /> (
Custom Rules