import { useCallback, useEffect, forwardRef, useMemo, useRef, useState, type Dispatch, type FormEvent, type ReactNode, type SetStateAction, } from "react"; import { Activity, ArrowUpCircle, ArrowUpDown, Bot, Brain, Check, CircleAlert, ChevronDown, ChevronLeft, ChevronRight, Cloud, Clipboard, Cpu, Database, Eye, EyeOff, ExternalLink, Gem, Globe2, GripVertical, Grid3X3, HardDrive, Hexagon, ImageIcon, Layers, ListOrdered, Loader2, LogOut, MessageCircle, Mic, Moon, PauseCircle, PlayCircle, Plus, Orbit, Palette, Pencil, RotateCcw, Search, Server, ShieldCheck, SlidersHorizontal, Sparkles, Trash2, Triangle, Waves, X, Zap, type LucideIcon, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { channelUiPresentation } from "@/channel-plugins/registry"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; import { SkillsCatalogSettings } from "@/components/settings/SkillsCatalogSettings"; import { TokenUsageHeatmap } from "@/components/settings/TokenUsageHeatmap"; import { ToggleButton } from "@/components/settings/ToggleButton"; import { channelIsRunning, channelMatchesFilter, channelSearchText, localizedChannelDisplayName, type ChannelFilter, } from "@/components/settings/channels/ChannelIdentity"; import { ChannelCatalogRow, ChannelSetupPanel, } from "@/components/settings/channels/ChannelSetupPanel"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { isLoopbackHost } from "@/lib/network"; import { checkVersion, completeProviderOAuth, createModelConfiguration, createProviderSettings, deleteModelConfiguration, disableNanobotFeature, enableNanobotFeature, fetchApiService, fetchAutomations, fetchSettings, fetchSettingsUsage, fetchCliApps, fetchMcpPresets, fetchNanobotFeatures, fetchProviderModels, importMcpConfig, loginProviderOAuth, logoutProviderOAuth, migrateModelConfigurations, runAutomationAction, runCliAppAction, runMcpPresetAction, saveCustomMcpServer, startApiService, stopApiService, updateAutomation, updateImageGenerationSettings, updateMcpServerTools, updateModelCallOrder, updateModelConfiguration, updateNetworkSafetySettings, updateProviderSettings, updateSettings, updateTranscriptionSettings, updateWebSearchSettings, } from "@/lib/api"; import { notifyCliAppsChanged } from "@/lib/cli-app-events"; import { copyTextToClipboard } from "@/lib/clipboard"; import { readLocalPreferences, writeLocalPreferences, type FileEditDisplayMode, type LocalActivityMode, type LocalDensity, type LocalPreferences, } from "@/lib/local-preferences"; import { getRuntimeHost, isNativeRuntime } from "@/lib/runtime"; import { notifyMcpPresetsChanged } from "@/lib/mcp-preset-events"; import { fmtDateTime, relativeTime } from "@/lib/format"; import { useLogoFallback } from "@/hooks/useLogoFallback"; import { useMediaQuery } from "@/hooks/useMediaQuery"; import { usePageVisibility } from "@/hooks/usePageVisibility"; import { isGenericRepositoryLogoUrl, logoFallbackUrls, providerBrand, providerDisplayLabel, } from "@/lib/provider-brand"; import { cn } from "@/lib/utils"; import { shortWorkspacePath } from "@/lib/workspace"; import { useClient } from "@/providers/ClientProvider"; import type { ApiServicePayload, AutomationsPayload, AutomationUpdatePayload, CliAppInfo, CliAppsPayload, ImageGenerationSettingsUpdate, McpPresetInfo, McpPresetsPayload, NanobotFeatureInfo, NanobotFeaturesPayload, NetworkSafetySettingsUpdate, ProviderModelsPayload, ProviderOAuthAuthorizationRequired, ProviderOAuthCompletionResult, ProviderOAuthLoginResult, ProviderOAuthPending, ProviderSettingsUpdate, SessionAutomationJob, SettingsPayload, SkillSummary, TranscriptionSettingsUpdate, WebSearchSettingsUpdate, WebuiDefaultAccessMode, } from "@/lib/types"; export type SettingsSectionKey = | "overview" | "appearance" | "models" | "image" | "voice" | "browser" | "channels" | "apps" | "automations" | "skills" | "runtime" | "advanced"; function isProviderOAuthAuthorizationRequired( payload: ProviderOAuthLoginResult, ): payload is ProviderOAuthAuthorizationRequired { return (payload as ProviderOAuthAuthorizationRequired).status === "authorization_required"; } function isProviderOAuthPending( payload: ProviderOAuthCompletionResult, ): payload is ProviderOAuthPending { return (payload as ProviderOAuthPending).status === "pending"; } type AppsKindFilter = "ready" | "cli" | "mcp"; type AutomationFilter = "all" | "active" | "paused" | "failed" | "system"; type AutomationSort = "next" | "last" | "updated" | "name"; type AutomationAction = "enable" | "disable" | "delete" | "run"; type AppsCatalogItem = | { id: string; kind: "cli"; app: CliAppInfo } | { id: string; kind: "mcp"; preset: McpPresetInfo }; interface AgentSettingsDraft { model: string; provider: string; modelPreset: string; presetLabel: string; maxTokens: number; contextWindowTokens: number; temperature: number; reasoningEffort: string; timezone: string; botName: string; botIcon: string; toolHintMaxLength: number; } type PendingRestartSection = "runtime" | "browser" | "image"; type PendingRestartSections = Record; type RestartAwarePayload = { requires_restart?: boolean; surface?: SettingsPayload["surface"]; runtime_surface?: SettingsPayload["runtime_surface"]; runtime_capabilities?: SettingsPayload["runtime_capabilities"]; }; type ProviderApiType = "auto" | "chat_completions" | "responses"; type ProviderAdvancedField = NonNullable< SettingsPayload["providers"][number]["advanced_fields"] >[number]; type ProviderForm = { displayName: string; apiKey: string; apiBase: string; apiType: ProviderApiType; proxy: string; extraHeaders: string; extraBody: string; extraQuery: string; thinkingStyle: string; region: string; profile: string; }; type CustomProviderDraft = ProviderForm & { name: string }; type CustomMcpTransport = "stdio" | "streamableHttp" | "sse"; const CONTEXT_WINDOW_TOKEN_OPTIONS = [65_536, 200_000, 262_144, 500_000, 1_048_576] as const; const OAUTH_PROXY_PROVIDERS = new Set(["openai_codex", "xai_grok"]); const CUSTOM_PROVIDER_CREATION_KEY = "__custom_provider__"; const CUSTOM_PROVIDER_ADVANCED_FIELDS: ProviderAdvancedField[] = [ "extra_headers", "extra_body", "extra_query", "proxy", "thinking_style", ]; const DEFERRED_MODEL_LIST_PROVIDERS = new Set([ "aihubmix", "atomic_chat", "byteplus", "byteplus_coding_plan", "huggingface", "lm_studio", "modelscope", "novita", "ollama", "openrouter", "ovms", "siliconflow", "vllm", "volcengine", "volcengine_coding_plan", ]); function providerJsonValue(value: Record | null | undefined): string { return value && Object.keys(value).length > 0 ? JSON.stringify(value, null, 2) : ""; } function providerFormFromRow( provider: SettingsPayload["providers"][number], ): ProviderForm { return { displayName: provider.is_custom ? provider.label : "", apiKey: "", apiBase: provider.api_base ?? provider.default_api_base ?? "", apiType: provider.api_type ?? "auto", proxy: provider.proxy ?? "", extraHeaders: providerJsonValue(provider.extra_headers), extraBody: providerJsonValue(provider.extra_body), extraQuery: providerJsonValue(provider.extra_query), thinkingStyle: provider.thinking_style ?? "", region: provider.region ?? "", profile: provider.profile ?? "", }; } function emptyCustomProviderDraft(): CustomProviderDraft { return { name: "", displayName: "", apiKey: "", apiBase: "", apiType: "auto", proxy: "", extraHeaders: "", extraBody: "", extraQuery: "", thinkingStyle: "", region: "", profile: "", }; } const DEFERRED_MODEL_LIST_QUERY_MIN_LENGTH = 2; const CLI_APPS_REFRESH_RETRY_MS = 2_000; const CLI_APPS_REFRESH_MAX_RETRIES = 30; const SETTINGS_SEARCH_INPUT_CLASS = cn( "border-border/45 bg-settings-surface transition-colors hover:border-border/70", "focus-visible:border-border/70 focus-visible:bg-background", "focus-visible:ring-0 focus-visible:ring-offset-0", ); const FALLBACK_TIMEZONES = [ "UTC", "Asia/Shanghai", "Asia/Hong_Kong", "Asia/Tokyo", "Asia/Seoul", "Asia/Singapore", "Asia/Taipei", "Asia/Dubai", "Asia/Kolkata", "Europe/London", "Europe/Paris", "Europe/Berlin", "Europe/Amsterdam", "America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles", "America/Toronto", "America/Sao_Paulo", "Australia/Sydney", "Pacific/Auckland", ]; interface CustomMcpForm { name: string; transport: CustomMcpTransport; command: string; args: string; url: string; env: string; headers: string; toolTimeout: string; } const OPENAI_API_TYPE_OPTIONS: Array<{ value: ProviderApiType; label: string }> = [ { value: "auto", label: "Auto" }, { value: "chat_completions", label: "Chat Completions" }, { value: "responses", label: "Responses" }, ]; const LOCAL_UNCONFIGURED_PROVIDER_ORDER = new Map( ["vllm", "ollama", "lm_studio", "atomic_chat", "ovms"].map((name, index) => [ name, index, ]), ); const IMAGE_ASPECT_RATIO_OPTIONS = ["1:1", "3:4", "9:16", "4:3", "16:9", "3:2", "2:3", "21:9"]; const IMAGE_SIZE_OPTIONS = ["1K", "2K", "4K", "1024x1024", "1536x1024", "1024x1536"]; const EMPTY_PENDING_RESTART_SECTIONS: PendingRestartSections = { runtime: false, browser: false, image: false, }; const DEFAULT_CUSTOM_MCP_FORM: CustomMcpForm = { name: "", transport: "stdio", command: "", args: "", url: "", env: "", headers: "", toolTimeout: "30", }; interface SettingsViewProps { theme: "light" | "dark"; initialSection?: SettingsSectionKey; initialSettings?: SettingsPayload | null; showSidebar?: boolean; onToggleTheme: () => void; onBackToChat: () => void; onModelNameChange: (modelName: string | null) => void; onSettingsChange?: (payload: SettingsPayload) => void; skills?: SkillSummary[]; onWorkspaceSettingsChange?: () => void | Promise; onSectionChange?: (section: SettingsSectionKey) => void; onLogout?: () => void; onRestart?: () => void; onNativeEngineRestart?: () => Promise; isRestarting?: boolean; hostChromeInset?: boolean; } function modelPresetValue(payload: SettingsPayload): string { return ( payload.model_call_order?.[0] ?? payload.model_presets.find((preset) => !preset.is_default)?.name ?? "" ); } function normalizeContextWindowTokens(value: number | null | undefined): number { return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 200_000; } function settingsProviderRow( payload: SettingsPayload, provider: string | null | undefined, ): SettingsPayload["providers"][number] | null { if (!provider) return null; return payload.providers.find((row) => row.name === provider) ?? null; } function settingsProviderConfigured( payload: SettingsPayload, provider: string | null | undefined, resolvedProvider?: string | null, ): boolean { const row = settingsProviderRow(payload, provider); if (row) return row.configured; if (provider === "auto") { const resolvedRow = settingsProviderRow( payload, resolvedProvider ?? payload.agent.resolved_provider ?? payload.agent.provider, ); if (resolvedRow) return resolvedRow.configured; } return payload.agent.has_api_key; } const DEFAULT_AGENT_SETTINGS_DRAFT: AgentSettingsDraft = { model: "", provider: "", modelPreset: "", presetLabel: "", maxTokens: 8192, contextWindowTokens: 200_000, temperature: 0.1, reasoningEffort: "", timezone: "UTC", botName: "nanobot", botIcon: "", toolHintMaxLength: 40, }; const DEFAULT_WEB_SEARCH_FORM: WebSearchSettingsUpdate = { provider: "duckduckgo", apiKey: "", baseUrl: "", maxResults: 5, timeout: 30, useJinaReader: true, }; const DEFAULT_IMAGE_GENERATION_FORM: ImageGenerationSettingsUpdate = { enabled: false, provider: "openrouter", model: "openai/gpt-5.4-image-2", defaultAspectRatio: "1:1", defaultImageSize: "1K", maxImagesPerTurn: 4, }; const DEFAULT_TRANSCRIPTION_FORM: TranscriptionSettingsUpdate = { enabled: true, provider: "groq", model: "", language: "", maxDurationSec: 120, maxUploadMb: 25, }; const DEFAULT_TRANSCRIPTION_SETTINGS: NonNullable = { enabled: true, provider: "groq", provider_configured: false, model: "whisper-large-v3", language: null, max_duration_sec: 120, max_upload_mb: 25, providers: [], }; const DEFAULT_NETWORK_SAFETY_FORM: NetworkSafetySettingsUpdate = { webuiAllowLocalServiceAccess: true, webuiDefaultAccessMode: "default", }; function agentDraftFromPayload( payload: SettingsPayload, preferredPresetName?: string, ): AgentSettingsDraft { const activePresetName = preferredPresetName ?? modelPresetValue(payload); const activePreset = payload.model_presets.find( (preset) => !preset.is_default && preset.name === activePresetName, ) ?? null; return { model: activePreset?.model ?? payload.agent.model, provider: activePreset?.provider ?? payload.agent.provider ?? payload.agent.resolved_provider ?? "", modelPreset: activePresetName, presetLabel: activePreset?.label ?? activePresetName, maxTokens: activePreset?.max_tokens ?? payload.agent.max_tokens, contextWindowTokens: normalizeContextWindowTokens( activePreset?.context_window_tokens ?? payload.agent.context_window_tokens, ), temperature: activePreset?.temperature ?? payload.agent.temperature, reasoningEffort: activePreset?.reasoning_effort ?? "", timezone: payload.agent.timezone, botName: payload.agent.bot_name, botIcon: payload.agent.bot_icon, toolHintMaxLength: payload.agent.tool_hint_max_length, }; } function webSearchFormFromPayload( payload: SettingsPayload, previous?: WebSearchSettingsUpdate, ): WebSearchSettingsUpdate { return { provider: payload.web_search.provider, apiKey: previous?.provider === payload.web_search.provider ? previous.apiKey ?? "" : "", baseUrl: payload.web_search.base_url ?? "", maxResults: payload.web_search.max_results, timeout: payload.web_search.timeout, useJinaReader: payload.web.fetch.use_jina_reader, }; } type WebSearchProviderOption = SettingsPayload["web_search"]["providers"][number]; function webSearchProviderAcceptsApiKey(provider?: WebSearchProviderOption): boolean { return provider?.credential === "api_key" || provider?.credential === "optional_api_key"; } function webSearchProviderRequiresApiKey(provider?: WebSearchProviderOption): boolean { return provider?.credential === "api_key"; } function imageGenerationFormFromPayload(payload: SettingsPayload): ImageGenerationSettingsUpdate { return { enabled: payload.image_generation.enabled, provider: payload.image_generation.provider, model: payload.image_generation.model, defaultAspectRatio: payload.image_generation.default_aspect_ratio, defaultImageSize: payload.image_generation.default_image_size, maxImagesPerTurn: payload.image_generation.max_images_per_turn, }; } function transcriptionFormFromPayload(payload: SettingsPayload): TranscriptionSettingsUpdate { const transcription = payload.transcription ?? DEFAULT_TRANSCRIPTION_SETTINGS; return { enabled: transcription.enabled, provider: transcription.provider, model: transcription.model, language: transcription.language ?? "", maxDurationSec: transcription.max_duration_sec, maxUploadMb: transcription.max_upload_mb, }; } function networkSafetyFormFromPayload(payload: SettingsPayload): NetworkSafetySettingsUpdate { return { webuiAllowLocalServiceAccess: payload.advanced.webui_allow_local_service_access ?? payload.advanced.allow_local_preview_access ?? true, webuiDefaultAccessMode: visibleWebuiDefaultAccessMode( payload.advanced.webui_default_access_mode, ), }; } function pendingRestartSectionsFromPayload(payload: SettingsPayload): PendingRestartSections { const sections = payload.restart_required_sections ?? []; return { runtime: sections.includes("runtime"), browser: sections.includes("browser"), image: sections.includes("image"), }; } export function SettingsView({ theme, initialSection = "overview", initialSettings = null, showSidebar = true, onToggleTheme, onBackToChat, onModelNameChange, onSettingsChange, skills = [], onWorkspaceSettingsChange, onSectionChange, onLogout, onRestart, onNativeEngineRestart, isRestarting = false, hostChromeInset = false, }: SettingsViewProps) { const { t } = useTranslation(); const { token } = useClient(); const pageVisible = usePageVisibility(); const remoteBrowserAccess = typeof window !== "undefined" && !isLoopbackHost(window.location.hostname); const [settings, setSettings] = useState(() => initialSettings); const [cliApps, setCliApps] = useState(null); const [nanobotFeatures, setNanobotFeatures] = useState(null); const featureCatalog = nanobotFeatures?.features ?? []; const [mcpPresets, setMcpPresets] = useState(null); const [automations, setAutomations] = useState(null); const [loading, setLoading] = useState(() => initialSettings === null); const [cliAppsLoading, setCliAppsLoading] = useState(true); const [nanobotFeaturesLoading, setNanobotFeaturesLoading] = useState(true); const [mcpPresetsLoading, setMcpPresetsLoading] = useState(true); const [automationsLoading, setAutomationsLoading] = useState(false); const [saving, setSaving] = useState(false); const [modelPresetCreating, setModelPresetCreating] = useState(false); const [modelConfigurationSaving, setModelConfigurationSaving] = useState(false); const [modelCallOrderSaving, setModelCallOrderSaving] = useState(false); const [modelMigrationSaving, setModelMigrationSaving] = useState(false); const [modelPresetPendingDelete, setModelPresetPendingDelete] = useState(null); const modelPresetBeforeCreateRef = useRef(null); const [cliAppsAction, setCliAppsAction] = useState(null); const [nanobotFeatureAction, setNanobotFeatureAction] = useState(null); const [nanobotFeatureConfirm, setNanobotFeatureConfirm] = useState(null); const [mcpPresetAction, setMcpPresetAction] = useState(null); const [providerSaving, setProviderSaving] = useState(null); const [xaiOAuthFlow, setXaiOAuthFlow] = useState(null); const xaiOAuthFlowRef = useRef(null); const [xaiOAuthCode, setXaiOAuthCode] = useState(""); const [xaiOAuthCompleting, setXaiOAuthCompleting] = useState(false); const [webSearchSaving, setWebSearchSaving] = useState(false); const [imageGenerationSaving, setImageGenerationSaving] = useState(false); const [transcriptionSaving, setTranscriptionSaving] = useState(false); const [networkSafetySaving, setNetworkSafetySaving] = useState(false); const [apiService, setApiService] = useState(null); const [apiServiceLoading, setApiServiceLoading] = useState(false); const [apiServiceAction, setApiServiceAction] = useState<"start" | "stop" | null>(null); const [apiServiceError, setApiServiceError] = useState(null); const [hostEngineApplying, setHostEngineApplying] = useState(false); const [error, setError] = useState(null); const [activeSection, setActiveSection] = useState(initialSection); const [expandedProvider, setExpandedProvider] = useState(null); const [appsQuery, setAppsQuery] = useState(""); const [channelsQuery, setChannelsQuery] = useState(""); const [automationsQuery, setAutomationsQuery] = useState(""); const [automationsFilter, setAutomationsFilter] = useState("all"); const [automationsSort, setAutomationsSort] = useState("next"); const [cliAppsMessage, setCliAppsMessage] = useState(null); const [cliAppsError, setCliAppsError] = useState(null); const [nanobotFeaturesError, setNanobotFeaturesError] = useState(null); const [cliAppsFocusName, setCliAppsFocusName] = useState(null); const [appsKindFilter, setAppsKindFilter] = useState("cli"); const [mcpMessage, setMcpMessage] = useState(null); const [mcpError, setMcpError] = useState(null); const [automationsError, setAutomationsError] = useState(null); const [automationAction, setAutomationAction] = useState(null); const [automationPendingDelete, setAutomationPendingDelete] = useState(null); const [automationPendingEdit, setAutomationPendingEdit] = useState(null); const [mcpFieldValues, setMcpFieldValues] = useState>>({}); const [customMcpForm, setCustomMcpForm] = useState(DEFAULT_CUSTOM_MCP_FORM); const [mcpConfigImport, setMcpConfigImport] = useState(""); const [providerForms, setProviderForms] = useState>({}); const [visibleProviderKeys, setVisibleProviderKeys] = useState>({}); const [editingProviderKeys, setEditingProviderKeys] = useState>({}); const [pendingRestartSections, setPendingRestartSections] = useState( EMPTY_PENDING_RESTART_SECTIONS, ); const [localPrefs, setLocalPrefs] = useState(() => readLocalPreferences()); const [webSearchForm, setWebSearchForm] = useState(() => initialSettings ? webSearchFormFromPayload(initialSettings) : DEFAULT_WEB_SEARCH_FORM, ); const [imageGenerationForm, setImageGenerationForm] = useState( () => initialSettings ? imageGenerationFormFromPayload(initialSettings) : DEFAULT_IMAGE_GENERATION_FORM, ); const [transcriptionForm, setTranscriptionForm] = useState( () => initialSettings ? transcriptionFormFromPayload(initialSettings) : DEFAULT_TRANSCRIPTION_FORM, ); const [networkSafetyForm, setNetworkSafetyForm] = useState(() => initialSettings ? networkSafetyFormFromPayload(initialSettings) : DEFAULT_NETWORK_SAFETY_FORM, ); useEffect(() => { setActiveSection(initialSection); }, [initialSection]); const selectSection = useCallback( (section: SettingsSectionKey) => { setActiveSection(section); onSectionChange?.(section); }, [onSectionChange], ); const [webSearchKeyVisible, setWebSearchKeyVisible] = useState(false); const [webSearchKeyEditing, setWebSearchKeyEditing] = useState(false); const [form, setForm] = useState(() => initialSettings ? agentDraftFromPayload(initialSettings) : DEFAULT_AGENT_SETTINGS_DRAFT, ); const [modelCallOrder, setModelCallOrder] = useState( () => initialSettings?.model_call_order ?? [], ); const applyPayload = useCallback( ( payload: SettingsPayload, options: { preserveAgentForm?: boolean } = {}, ) => { setSettings(payload); if (!options.preserveAgentForm) { setForm(agentDraftFromPayload(payload)); setModelPresetCreating(false); } setModelCallOrder(payload.model_call_order ?? []); setWebSearchForm((prev) => webSearchFormFromPayload(payload, prev)); setImageGenerationForm(imageGenerationFormFromPayload(payload)); setTranscriptionForm(transcriptionFormFromPayload(payload)); setNetworkSafetyForm(networkSafetyFormFromPayload(payload)); if (payload.restart_required_sections) { setPendingRestartSections(pendingRestartSectionsFromPayload(payload)); } onSettingsChange?.(payload); }, [onSettingsChange], ); const closeXaiOAuthFlow = useCallback(() => { xaiOAuthFlowRef.current = null; setXaiOAuthFlow(null); setXaiOAuthCode(""); setXaiOAuthCompleting(false); }, []); useEffect(() => { if (!xaiOAuthFlow) return; let cancelled = false; let timer: number | null = null; const poll = async () => { try { const payload = await completeProviderOAuth( token, xaiOAuthFlow.provider, xaiOAuthFlow.flow_id, ); if (cancelled || xaiOAuthFlowRef.current?.flow_id !== xaiOAuthFlow.flow_id) return; if (isProviderOAuthPending(payload)) { timer = window.setTimeout(() => void poll(), 1000); return; } applyPayload(payload); setExpandedProvider(xaiOAuthFlow.provider); setError(null); closeXaiOAuthFlow(); } catch (err) { if (cancelled || xaiOAuthFlowRef.current?.flow_id !== xaiOAuthFlow.flow_id) return; setError((err as Error).message); closeXaiOAuthFlow(); } }; timer = window.setTimeout(() => void poll(), 1000); return () => { cancelled = true; if (timer !== null) window.clearTimeout(timer); }; }, [applyPayload, closeXaiOAuthFlow, token, xaiOAuthFlow]); useEffect(() => { if (!initialSettings || settings !== null) return; applyPayload(initialSettings); setLoading(false); }, [applyPayload, initialSettings, settings]); useEffect(() => { let cancelled = false; const showLoading = settings === null; if (showLoading) setLoading(true); fetchSettings(token) .then((payload) => { if (!cancelled) { applyPayload(payload); setError(null); } }) .catch((err) => { if (!cancelled && showLoading) setError((err as Error).message); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [applyPayload, token]); const hasSettings = settings !== null; useEffect(() => { if (activeSection !== "overview" || !hasSettings || !pageVisible) return; let cancelled = false; const refresh = () => { fetchSettingsUsage(token) .then((usage) => { if (cancelled) return; setSettings((current) => (current ? { ...current, usage } : current)); }) .catch(() => {}); }; void refresh(); const interval = window.setInterval(refresh, 5000); const onFocus = () => refresh(); window.addEventListener("focus", onFocus); return () => { cancelled = true; window.clearInterval(interval); window.removeEventListener("focus", onFocus); }; }, [activeSection, hasSettings, pageVisible, token]); useEffect(() => { if (activeSection !== "apps") return; let cancelled = false; let retry: number | null = null; let retryCount = 0; const loadCliApps = (showLoading: boolean) => { if (showLoading) setCliAppsLoading(true); fetchCliApps(token) .then((payload) => { if (cancelled) return; if (payload.catalog_refresh_pending && retryCount < CLI_APPS_REFRESH_MAX_RETRIES) { retryCount += 1; retry = window.setTimeout(() => { retry = null; loadCliApps(false); }, CLI_APPS_REFRESH_RETRY_MS); } setCliApps(payload); setCliAppsError(null); setCliAppsLoading(false); }) .catch((err) => { if (!cancelled) { setCliAppsError((err as Error).message); setCliAppsLoading(false); } }); }; loadCliApps(true); return () => { cancelled = true; if (retry !== null) window.clearTimeout(retry); }; }, [activeSection, token]); useEffect(() => { if (!["channels", "models", "browser", "runtime"].includes(activeSection)) return; let cancelled = false; const refresh = async (showLoading = false) => { if (showLoading) setNanobotFeaturesLoading(true); try { const payload = await fetchNanobotFeatures(token); if (!cancelled) { setNanobotFeatures(payload); setNanobotFeaturesError(null); } } catch (err) { const message = (err as Error).message; if (!cancelled && message !== "HTTP 404") setNanobotFeaturesError(message); } finally { if (!cancelled && showLoading) setNanobotFeaturesLoading(false); } }; void refresh(true); const interval = activeSection === "channels" ? window.setInterval(() => void refresh(false), 5000) : null; const refreshOnFocus = () => { if (activeSection === "channels" && document.visibilityState !== "hidden") { void refresh(false); } }; window.addEventListener("focus", refreshOnFocus); document.addEventListener("visibilitychange", refreshOnFocus); return () => { cancelled = true; if (interval !== null) window.clearInterval(interval); window.removeEventListener("focus", refreshOnFocus); document.removeEventListener("visibilitychange", refreshOnFocus); }; }, [activeSection, token]); useEffect(() => { if (activeSection !== "runtime") return; let cancelled = false; setApiServiceLoading(true); fetchApiService(token) .then((payload) => { if (!cancelled) { setApiService(payload); setApiServiceError(null); } }) .catch((err) => { if (!cancelled) setApiServiceError((err as Error).message); }) .finally(() => { if (!cancelled) setApiServiceLoading(false); }); return () => { cancelled = true; }; }, [activeSection, token]); useEffect(() => { if (activeSection !== "apps") return; let cancelled = false; setMcpPresetsLoading(true); fetchMcpPresets(token) .then((payload) => { if (!cancelled) { setMcpPresets(payload); setMcpError(null); } }) .catch((err) => { if (!cancelled) setMcpError((err as Error).message); }) .finally(() => { if (!cancelled) setMcpPresetsLoading(false); }); return () => { cancelled = true; }; }, [activeSection, token]); const refreshAutomations = useCallback( async (showLoading = false) => { if (showLoading) setAutomationsLoading(true); try { const payload = await fetchAutomations(token); setAutomations(payload); setAutomationsError(null); } catch (err) { setAutomationsError((err as Error).message); } finally { if (showLoading) setAutomationsLoading(false); } }, [token], ); useEffect(() => { if (activeSection !== "automations" || !pageVisible) return; let cancelled = false; const refresh = async (showLoading = false) => { if (cancelled) return; if (showLoading) setAutomationsLoading(true); try { const payload = await fetchAutomations(token); if (cancelled) return; setAutomations(payload); setAutomationsError(null); } catch (err) { if (!cancelled) setAutomationsError((err as Error).message); } finally { if (!cancelled && showLoading) setAutomationsLoading(false); } }; void refresh(true); const interval = window.setInterval(() => void refresh(false), 5000); const refreshOnFocus = () => void refresh(false); window.addEventListener("focus", refreshOnFocus); return () => { cancelled = true; window.clearInterval(interval); window.removeEventListener("focus", refreshOnFocus); }; }, [activeSection, pageVisible, token]); useEffect(() => { writeLocalPreferences(localPrefs); }, [localPrefs]); useEffect(() => { if (!settings) return; setProviderForms((prev) => { const next = { ...prev }; for (const provider of settings.providers) { next[provider.name] = next[provider.name] ?? providerFormFromRow(provider); } return next; }); }, [settings]); const modelDirty = useMemo(() => { if (!settings) return false; const selectedPreset = settings.model_presets.find( (preset) => !preset.is_default && preset.name === form.modelPreset, ); if (!selectedPreset) return false; return ( form.model !== selectedPreset.model || form.provider !== selectedPreset.provider || form.maxTokens !== selectedPreset.max_tokens || form.contextWindowTokens !== normalizeContextWindowTokens(selectedPreset.context_window_tokens) || form.temperature !== selectedPreset.temperature || form.reasoningEffort !== (selectedPreset.reasoning_effort ?? "") || form.presetLabel.trim() !== selectedPreset.label ); }, [form, settings]); const runtimeDirty = useMemo(() => { if (!settings) return false; return ( form.timezone !== settings.agent.timezone || form.botName !== settings.agent.bot_name || form.botIcon !== settings.agent.bot_icon ); }, [form, settings]); const imageGenerationDirty = useMemo(() => { if (!settings) return false; return ( imageGenerationForm.enabled !== settings.image_generation.enabled || imageGenerationForm.provider !== settings.image_generation.provider || imageGenerationForm.model !== settings.image_generation.model || imageGenerationForm.defaultAspectRatio !== settings.image_generation.default_aspect_ratio || imageGenerationForm.defaultImageSize !== settings.image_generation.default_image_size || imageGenerationForm.maxImagesPerTurn !== settings.image_generation.max_images_per_turn ); }, [imageGenerationForm, settings]); const transcriptionDirty = useMemo(() => { if (!settings) return false; const transcription = settings.transcription ?? DEFAULT_TRANSCRIPTION_SETTINGS; return ( transcriptionForm.enabled !== transcription.enabled || transcriptionForm.provider !== transcription.provider || transcriptionForm.model !== transcription.model || transcriptionForm.language !== (transcription.language ?? "") || transcriptionForm.maxDurationSec !== transcription.max_duration_sec || transcriptionForm.maxUploadMb !== transcription.max_upload_mb ); }, [settings, transcriptionForm]); const networkSafetyDirty = useMemo(() => { if (!settings) return false; const currentLocalServiceAccess = settings.advanced.webui_allow_local_service_access ?? settings.advanced.allow_local_preview_access ?? true; const currentDefaultAccess = visibleWebuiDefaultAccessMode(settings.advanced.webui_default_access_mode); return ( networkSafetyForm.webuiAllowLocalServiceAccess !== currentLocalServiceAccess || networkSafetyForm.webuiDefaultAccessMode !== currentDefaultAccess ); }, [networkSafetyForm, settings]); const configuredModelProviderOptions = useMemo( () => settings?.providers .filter((provider) => provider.configured && provider.model_selectable !== false) .map((provider) => ({ name: provider.name, label: provider.label })) ?? [], [settings], ); const hasPendingRestart = useMemo( () => !!settings?.requires_restart || pendingRestartSections.runtime || pendingRestartSections.browser || pendingRestartSections.image, [pendingRestartSections, settings?.requires_restart], ); const restartViaSettingsSurface = useCallback(async () => { const isNativeHost = (settings?.surface ?? settings?.runtime_surface) === "native"; if ( isNativeHost && settings?.runtime_capabilities?.can_restart_engine && onNativeEngineRestart ) { setHostEngineApplying(true); try { const nextToken = await onNativeEngineRestart(); const payload = await fetchSettings(nextToken); applyPayload(payload); setPendingRestartSections(EMPTY_PENDING_RESTART_SECTIONS); setError(null); } catch (err) { setError((err as Error).message); } finally { setHostEngineApplying(false); } return; } onRestart?.(); }, [applyPayload, onNativeEngineRestart, onRestart, settings]); const maybeRestartHostEngine = useCallback( async (payload: RestartAwarePayload) => { const surface = payload.surface ?? payload.runtime_surface ?? settings?.surface ?? settings?.runtime_surface; const capabilities = payload.runtime_capabilities ?? settings?.runtime_capabilities; const isNativeHost = surface === "native"; if ( !payload.requires_restart || !isNativeHost || !capabilities?.can_restart_engine || !onNativeEngineRestart ) { return; } setHostEngineApplying(true); try { const nextToken = await onNativeEngineRestart(); const refreshed = await fetchSettings(nextToken); applyPayload(refreshed); setPendingRestartSections(EMPTY_PENDING_RESTART_SECTIONS); setError(null); } catch (err) { setError((err as Error).message); } finally { setHostEngineApplying(false); } }, [applyPayload, onNativeEngineRestart, settings], ); const saveModelSettings = async () => { if ( !settings || saving || modelCallOrderSaving || modelConfigurationSaving ) { return; } if (modelPresetCreating) { const label = form.presetLabel.trim(); const provider = form.provider.trim(); const model = form.model.trim(); if ( !label || !provider || !model || form.maxTokens <= 0 || form.contextWindowTokens <= 0 || form.temperature < 0 || form.temperature > 2 ) { return; } setModelConfigurationSaving(true); try { const payload = await createModelConfiguration(token, { label, provider, model, maxTokens: form.maxTokens, contextWindowTokens: form.contextWindowTokens, temperature: form.temperature, reasoningEffort: form.reasoningEffort || null, }); const createdPreset = payload.created_model_preset; const nextOrder = createdPreset ? [...modelCallOrder, createdPreset] : null; applyPayload(payload); if (createdPreset) { setForm(agentDraftFromPayload(payload, createdPreset)); } let finalPayload = payload; if (nextOrder) { const orderedPayload = await updateModelCallOrder(token, nextOrder); applyPayload(orderedPayload); finalPayload = orderedPayload; } if (createdPreset) { setForm(agentDraftFromPayload(finalPayload, createdPreset)); } modelPresetBeforeCreateRef.current = null; onModelNameChange(finalPayload.agent.model || null); setError(null); } catch (err) { setError((err as Error).message); } finally { setModelConfigurationSaving(false); } return; } if (!modelDirty) return; const selectedPreset = settings.model_presets.find( (preset) => !preset.is_default && preset.name === form.modelPreset, ); if (!selectedPreset) return; const reasoningEffort = form.reasoningEffort || null; setSaving(true); try { const payload = await updateModelConfiguration(token, { name: selectedPreset.name, label: form.presetLabel.trim() !== selectedPreset.label ? form.presetLabel.trim() : undefined, model: form.model !== selectedPreset.model ? form.model : undefined, provider: form.provider !== selectedPreset.provider ? form.provider : undefined, maxTokens: form.maxTokens !== selectedPreset.max_tokens ? form.maxTokens : undefined, contextWindowTokens: form.contextWindowTokens !== normalizeContextWindowTokens(selectedPreset.context_window_tokens) ? form.contextWindowTokens : undefined, temperature: form.temperature !== selectedPreset.temperature ? form.temperature : undefined, reasoningEffort: reasoningEffort !== selectedPreset.reasoning_effort ? reasoningEffort : undefined, }); applyPayload(payload); setForm(agentDraftFromPayload(payload, selectedPreset.name)); onModelNameChange(payload.agent.model || null); setError(null); } catch (err) { setError((err as Error).message); } finally { setSaving(false); } }; const beginModelPresetCreation = () => { if (!settings || saving || modelCallOrderSaving || modelConfigurationSaving) return; const primaryPreset = settings.model_presets.find( (preset) => !preset.is_default && preset.name === settings.model_call_order?.[0], ); const currentProvider = primaryPreset?.provider === "auto" ? primaryPreset.resolved_provider ?? settings.agent.resolved_provider : primaryPreset?.provider ?? settings.agent.provider; const provider = configuredModelProviderOptions.find((option) => option.name === currentProvider)?.name ?? configuredModelProviderOptions[0]?.name ?? ""; modelPresetBeforeCreateRef.current = form.modelPreset; setForm((prev) => ({ ...prev, modelPreset: "", presetLabel: "", provider, model: "", maxTokens: primaryPreset?.max_tokens ?? settings.agent.max_tokens, contextWindowTokens: normalizeContextWindowTokens( primaryPreset?.context_window_tokens ?? settings.agent.context_window_tokens, ), temperature: primaryPreset?.temperature ?? settings.agent.temperature, reasoningEffort: primaryPreset?.reasoning_effort ?? settings.agent.reasoning_effort ?? "", })); setModelPresetCreating(true); }; const cancelModelPresetCreation = () => { if (!settings || modelConfigurationSaving) return; const previousPreset = modelPresetBeforeCreateRef.current; setModelPresetCreating(false); setForm(agentDraftFromPayload(settings, previousPreset ?? undefined)); modelPresetBeforeCreateRef.current = null; }; const changeModelCallOrder = async (nextOrder: string[]) => { const unchanged = nextOrder.length === modelCallOrder.length && nextOrder.every((name, index) => name === modelCallOrder[index]); if ( !settings || saving || modelCallOrderSaving || modelConfigurationSaving || nextOrder.length === 0 || unchanged ) { return; } const previousOrder = [...modelCallOrder]; setModelCallOrder(nextOrder); setModelCallOrderSaving(true); try { const payload = await updateModelCallOrder(token, nextOrder); applyPayload(payload, { preserveAgentForm: true }); onModelNameChange(payload.agent.model || null); setError(null); } catch (err) { setModelCallOrder(previousOrder); setError((err as Error).message); } finally { setModelCallOrderSaving(false); } }; const handleMigrateModelConfigurations = async () => { if (modelMigrationSaving) return; setModelMigrationSaving(true); try { const payload = await migrateModelConfigurations(token); applyPayload(payload); onModelNameChange(payload.agent.model || null); setError(null); } catch (err) { setError((err as Error).message); } finally { setModelMigrationSaving(false); } }; const handleDeleteModelConfiguration = async () => { if ( !modelPresetPendingDelete || saving || modelCallOrderSaving || modelConfigurationSaving ) { return; } setSaving(true); try { const payload = await deleteModelConfiguration(token, modelPresetPendingDelete.name); applyPayload(payload); setModelPresetPendingDelete(null); setError(null); } catch (err) { setError((err as Error).message); } finally { setSaving(false); } }; const saveRuntimeSettings = async () => { if (!settings || !runtimeDirty || saving) return; setSaving(true); try { const payload = await updateSettings(token, { timezone: form.timezone, botName: form.botName, botIcon: form.botIcon, }); applyPayload(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, runtime: true })); } await onWorkspaceSettingsChange?.(); await maybeRestartHostEngine(payload); setError(null); } catch (err) { setError((err as Error).message); } finally { setSaving(false); } }; const saveImageGenerationSettings = async () => { if (!settings || !imageGenerationDirty || imageGenerationSaving) return; setImageGenerationSaving(true); try { const payload = await updateImageGenerationSettings(token, imageGenerationForm); applyPayload(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, image: true })); } await maybeRestartHostEngine(payload); setError(null); } catch (err) { setError((err as Error).message); } finally { setImageGenerationSaving(false); } }; const saveTranscriptionSettings = async () => { if (!settings || !transcriptionDirty || transcriptionSaving) return; setTranscriptionSaving(true); try { const payload = await updateTranscriptionSettings(token, transcriptionForm); applyPayload(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, browser: true })); } await maybeRestartHostEngine(payload); setError(null); } catch (err) { setError((err as Error).message); } finally { setTranscriptionSaving(false); } }; const saveNetworkSafetySettings = async () => { if (!settings || !networkSafetyDirty || networkSafetySaving) return; setNetworkSafetySaving(true); try { const payload = await updateNetworkSafetySettings(token, networkSafetyForm); applyPayload(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, runtime: true })); } await maybeRestartHostEngine(payload); setError(null); } catch (err) { setError((err as Error).message); } finally { setNetworkSafetySaving(false); } }; const installCapabilities = async (names: string[]): Promise => { const missing = names.filter( (name) => !featureCatalog.find((feature) => feature.name === name)?.installed, ); if (!missing.length) return true; setNanobotFeatureAction(`enable:${names.join("+")}`); setNanobotFeaturesError(null); try { let latest = nanobotFeatures; for (const name of missing) { latest = await enableNanobotFeature(token, name); if (latest.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, runtime: true })); } } if (latest) setNanobotFeatures(latest); return true; } catch (err) { setNanobotFeaturesError((err as Error).message); return false; } finally { setNanobotFeatureAction(null); } }; const handleApiServiceAction = async ( action: "start" | "stop", values?: { host: string; port: number; timeout: number; apiKey?: string }, ) => { if (apiServiceAction) return; setApiServiceAction(action); setApiServiceError(null); try { const payload = action === "start" ? await startApiService(token, values!) : await stopApiService(token); setApiService(payload); const refreshed = await fetchNanobotFeatures(token); setNanobotFeatures(refreshed); const nextSettings = await fetchSettings(token); applyPayload(nextSettings); } catch (err) { setApiServiceError((err as Error).message); } finally { setApiServiceAction(null); } }; const saveProvider = async (providerName: string) => { if (providerSaving) return; const provider = settings?.providers.find((item) => item.name === providerName); if (!provider) return; const isOauthProvider = provider.auth_type === "oauth"; const providerForm = providerForms[providerName] ?? providerFormFromRow(provider); const apiKey = providerForm.apiKey.trim(); const apiKeyRequired = provider.api_key_required ?? true; if (!isOauthProvider && !provider.configured && apiKeyRequired && !apiKey) { setError(t("settings.byok.apiKeyRequired")); return; } setProviderSaving(providerName); try { const supportName = providerName === "bedrock" ? "bedrock" : providerName === "azure_openai" ? "azure" : null; if (supportName && !(await installCapabilities([supportName]))) return; const update: ProviderSettingsUpdate = { provider: providerName }; if (!isOauthProvider) { update.apiKey = apiKey || undefined; update.apiBase = providerForm.apiBase.trim(); if (provider.is_custom) update.displayName = providerForm.displayName.trim(); } for (const field of provider.advanced_fields ?? []) { if (field === "api_type") update.apiType = providerForm.apiType; if (field === "proxy") update.proxy = providerForm.proxy.trim(); if (field === "extra_headers") { update.extraHeaders = providerForm.extraHeaders.trim(); } if (field === "extra_body") update.extraBody = providerForm.extraBody.trim(); if (field === "extra_query") update.extraQuery = providerForm.extraQuery.trim(); if (field === "thinking_style") { update.thinkingStyle = providerForm.thinkingStyle.trim(); } if (field === "region") update.region = providerForm.region.trim(); if (field === "profile") update.profile = providerForm.profile.trim(); } const payload = await updateProviderSettings(token, update); applyPayload(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, image: true })); } await maybeRestartHostEngine(payload); setProviderForms((prev) => ({ ...prev, [providerName]: { ...providerForm, displayName: providerForm.displayName.trim(), apiKey: "", apiBase: providerForm.apiBase.trim(), proxy: providerForm.proxy.trim(), thinkingStyle: providerForm.thinkingStyle.trim(), region: providerForm.region.trim(), profile: providerForm.profile.trim(), }, })); setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false })); setEditingProviderKeys((prev) => ({ ...prev, [providerName]: false })); if (!isOauthProvider) setExpandedProvider(null); setError(null); } catch (err) { setError((err as Error).message); } finally { setProviderSaving(null); } }; const createCustomProvider = async (draft: CustomProviderDraft): Promise => { if (providerSaving) return false; setProviderSaving(CUSTOM_PROVIDER_CREATION_KEY); try { const payload = await createProviderSettings(token, { name: draft.name.trim(), apiKey: draft.apiKey.trim() || undefined, apiBase: draft.apiBase.trim(), proxy: draft.proxy.trim(), extraHeaders: draft.extraHeaders.trim(), extraBody: draft.extraBody.trim(), extraQuery: draft.extraQuery.trim(), thinkingStyle: draft.thinkingStyle.trim(), }); applyPayload(payload); setExpandedProvider(null); setError(null); return true; } catch (err) { setError((err as Error).message); return false; } finally { setProviderSaving(null); } }; const runProviderOAuth = async (providerName: string, action: "login" | "logout") => { if (providerSaving) return; let popup: Window | null = null; if (action === "login" && providerName === "xai_grok" && !remoteBrowserAccess) { try { popup = window.open("about:blank", "_blank"); if (popup) popup.opener = null; } catch { popup = null; } } setProviderSaving(providerName); try { const payload = action === "login" ? await loginProviderOAuth(token, providerName) : await logoutProviderOAuth(token, providerName); if (isProviderOAuthAuthorizationRequired(payload)) { try { if (popup && !popup.closed) popup.location.href = payload.authorization_url; } catch { // The dialog keeps the authorization link available when the popup was closed. } xaiOAuthFlowRef.current = payload; setXaiOAuthFlow(payload); setXaiOAuthCode(""); setExpandedProvider(providerName); setError(null); return; } popup?.close(); closeXaiOAuthFlow(); applyPayload(payload); setExpandedProvider(providerName); setError(null); } catch (err) { popup?.close(); setError((err as Error).message); } finally { setProviderSaving(null); } }; const completeXaiOAuth = async () => { const flow = xaiOAuthFlowRef.current; const authorizationCode = xaiOAuthCode.trim(); if (!flow || !authorizationCode || xaiOAuthCompleting) return; setXaiOAuthCompleting(true); try { const payload = await completeProviderOAuth( token, flow.provider, flow.flow_id, authorizationCode, ); if (xaiOAuthFlowRef.current?.flow_id !== flow.flow_id) return; if (isProviderOAuthPending(payload)) return; applyPayload(payload); setExpandedProvider(flow.provider); setError(null); closeXaiOAuthFlow(); } catch (err) { if (xaiOAuthFlowRef.current?.flow_id === flow.flow_id) { setError((err as Error).message); closeXaiOAuthFlow(); } } finally { setXaiOAuthCompleting(false); } }; const saveWebSearch = async () => { if (!settings || webSearchSaving) return; const provider = settings.web_search.providers.find((item) => item.name === webSearchForm.provider); if (!provider) return; const apiKey = webSearchForm.apiKey?.trim() ?? ""; const baseUrl = webSearchForm.baseUrl?.trim() ?? ""; const hasExistingSecret = webSearchProviderAcceptsApiKey(provider) && webSearchForm.provider === settings.web_search.provider && !!settings.web_search.api_key_hint; if (webSearchProviderRequiresApiKey(provider) && !apiKey && !hasExistingSecret) { setError(t("settings.byok.webSearch.apiKeyRequired")); return; } if (provider.credential === "base_url" && !baseUrl) { setError(t("settings.byok.webSearch.baseUrlRequired")); return; } setWebSearchSaving(true); try { if (provider.name === "olostep" && !(await installCapabilities(["olostep"]))) return; const webFetchRestartRequired = (webSearchForm.useJinaReader ?? settings.web.fetch.use_jina_reader) !== settings.web.fetch.use_jina_reader; const update: WebSearchSettingsUpdate = { provider: webSearchForm.provider, maxResults: webSearchForm.maxResults, timeout: webSearchForm.timeout, useJinaReader: webSearchForm.useJinaReader, }; if ( webSearchProviderAcceptsApiKey(provider) && (apiKey || (provider.credential === "optional_api_key" && webSearchKeyEditing)) ) { update.apiKey = apiKey; } if (provider.credential === "base_url") update.baseUrl = baseUrl; const payload = await updateWebSearchSettings(token, update); applyPayload(payload); if (payload.requires_restart || webFetchRestartRequired) { setPendingRestartSections((prev) => ({ ...prev, browser: true })); } await maybeRestartHostEngine(payload); setWebSearchForm((prev) => ({ provider: payload.web_search.provider, apiKey: "", baseUrl: payload.web_search.base_url ?? prev.baseUrl ?? "", maxResults: payload.web_search.max_results, timeout: payload.web_search.timeout, useJinaReader: payload.web.fetch.use_jina_reader, })); setWebSearchKeyVisible(false); setWebSearchKeyEditing(false); setError(null); } catch (err) { setError((err as Error).message); } finally { setWebSearchSaving(false); } }; const resetProviderDraft = useCallback((providerName: string) => { const provider = settings?.providers.find((item) => item.name === providerName); if (!provider) return; setProviderForms((prev) => ({ ...prev, [providerName]: providerFormFromRow(provider), })); setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: false })); setEditingProviderKeys((prev) => ({ ...prev, [providerName]: false })); }, [settings]); const handleToggleProvider = useCallback((providerName: string) => { if (expandedProvider) resetProviderDraft(expandedProvider); setExpandedProvider(expandedProvider === providerName ? null : providerName); }, [expandedProvider, resetProviderDraft]); const resetWebSearchDraft = useCallback(() => { if (!settings) return; setWebSearchForm({ provider: settings.web_search.provider, apiKey: "", baseUrl: settings.web_search.base_url ?? "", maxResults: settings.web_search.max_results, timeout: settings.web_search.timeout, useJinaReader: settings.web.fetch.use_jina_reader, }); setWebSearchKeyVisible(false); setWebSearchKeyEditing(false); }, [settings]); const handleWebSearchProviderChange = useCallback((provider: string) => { if (!settings) return; setWebSearchForm((prev) => ({ provider, apiKey: "", baseUrl: provider === settings.web_search.provider ? settings.web_search.base_url ?? "" : "", maxResults: prev.maxResults ?? settings.web_search.max_results, timeout: prev.timeout ?? settings.web_search.timeout, useJinaReader: prev.useJinaReader ?? settings.web.fetch.use_jina_reader, })); setWebSearchKeyVisible(false); setWebSearchKeyEditing(false); }, [settings]); const toggleProviderKeyVisibility = (providerName: string) => { const isVisible = visibleProviderKeys[providerName]; setVisibleProviderKeys((prev) => ({ ...prev, [providerName]: !isVisible })); }; const toggleProviderKeyEditing = (providerName: string) => { setEditingProviderKeys((prev) => { const nextEditing = !prev[providerName]; if (!nextEditing) { setProviderForms((forms) => ({ ...forms, [providerName]: { ...(forms[providerName] ?? providerFormFromRow( settings?.providers.find((provider) => provider.name === providerName) ?? { name: providerName, label: providerName, configured: false, }, )), apiKey: "", }, })); setVisibleProviderKeys((visible) => ({ ...visible, [providerName]: false })); } return { ...prev, [providerName]: nextEditing }; }); }; const handleCliAppAction = async ( action: "install" | "update" | "uninstall" | "test", name: string, ) => { const key = `${action}:${name}`; setCliAppsAction(key); setCliAppsMessage(null); setCliAppsError(null); try { const payload = await runCliAppAction(token, action, name); setCliApps(payload); if (action !== "test") { notifyCliAppsChanged(payload); } setCliAppsMessage(payload.last_action?.message ?? null); setCliAppsFocusName(action === "uninstall" ? null : name); } catch (err) { setCliAppsError((err as Error).message); } finally { setCliAppsAction(null); } }; const handleNanobotFeatureAction = async ( action: "enable" | "disable", name: string, confirmed = false, ) => { const feature = featureCatalog.find((item) => item.name === name); if (action === "enable" && !confirmed && feature && !feature.installed && feature.install_supported) { setNanobotFeaturesError(null); setNanobotFeatureConfirm(feature); return; } const key = `${action}:${name}`; setNanobotFeatureAction(key); setNanobotFeatureConfirm(null); setNanobotFeaturesError(null); try { const payload = action === "enable" ? await enableNanobotFeature(token, name) : await disableNanobotFeature(token, name); setNanobotFeatures(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, runtime: true })); } } catch (err) { setNanobotFeaturesError((err as Error).message); } finally { setNanobotFeatureAction(null); } }; const handleAutomationAction = async ( action: AutomationAction, job: SessionAutomationJob, ) => { const key = `${action}:${job.id}`; setAutomationAction(key); setAutomationsError(null); try { const payload = await runAutomationAction(token, action, job.id); setAutomations(payload); if (action === "delete") setAutomationPendingDelete(null); if (action === "run") { window.setTimeout(() => void refreshAutomations(false), 1200); window.setTimeout(() => void refreshAutomations(false), 4000); } } catch (err) { setAutomationsError((err as Error).message); } finally { setAutomationAction(null); } }; const handleAutomationEdit = async ( job: SessionAutomationJob, values: AutomationUpdatePayload, ) => { const key = `update:${job.id}`; setAutomationAction(key); setAutomationsError(null); try { const payload = await updateAutomation(token, job.id, values); setAutomations(payload); setAutomationPendingEdit(null); } catch (err) { setAutomationsError((err as Error).message); } finally { setAutomationAction(null); } }; const handleMcpPresetAction = async ( action: "enable" | "remove" | "test", name: string, values: Record = {}, ) => { const key = `${action}:${name}`; setMcpPresetAction(key); setMcpMessage(null); setMcpError(null); try { const payload = await runMcpPresetAction(token, action, name, values); setMcpPresets(payload); setMcpMessage(payload.last_action?.message ?? null); if (action !== "test") { notifyMcpPresetsChanged(payload); } if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, runtime: true })); } await maybeRestartHostEngine(payload); if (action === "enable") { setMcpFieldValues((prev) => ({ ...prev, [name]: {} })); } } catch (err) { setMcpError((err as Error).message); } finally { setMcpPresetAction(null); } }; const handleSaveCustomMcp = async () => { const name = customMcpForm.name.trim(); const key = `custom:${name || "new"}`; setMcpPresetAction(key); setMcpMessage(null); setMcpError(null); try { const payload = await saveCustomMcpServer(token, { name, transport: customMcpForm.transport, command: customMcpForm.command, args: customMcpForm.args, url: customMcpForm.url, env: customMcpForm.env, headers: customMcpForm.headers, tool_timeout: customMcpForm.toolTimeout, }); setMcpPresets(payload); setMcpMessage(payload.last_action?.message ?? null); notifyMcpPresetsChanged(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, runtime: true })); } await maybeRestartHostEngine(payload); setCustomMcpForm((prev) => ({ ...DEFAULT_CUSTOM_MCP_FORM, transport: prev.transport })); } catch (err) { setMcpError((err as Error).message); } finally { setMcpPresetAction(null); } }; const handleImportMcpConfig = async () => { setMcpPresetAction("import"); setMcpMessage(null); setMcpError(null); try { const payload = await importMcpConfig(token, mcpConfigImport); setMcpPresets(payload); setMcpMessage(payload.last_action?.message ?? null); notifyMcpPresetsChanged(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, runtime: true })); } await maybeRestartHostEngine(payload); setMcpConfigImport(""); } catch (err) { setMcpError((err as Error).message); } finally { setMcpPresetAction(null); } }; const handleMcpToolsChange = async (name: string, enabledTools: string[]) => { setMcpPresetAction(`tools:${name}`); setMcpMessage(null); setMcpError(null); try { const payload = await updateMcpServerTools(token, name, enabledTools); setMcpPresets(payload); setMcpMessage(payload.last_action?.message ?? null); notifyMcpPresetsChanged(payload); if (payload.requires_restart) { setPendingRestartSections((prev) => ({ ...prev, runtime: true })); } await maybeRestartHostEngine(payload); } catch (err) { setMcpError((err as Error).message); } finally { setMcpPresetAction(null); } }; const renderSection = () => { if (!settings) return null; switch (activeSection) { case "overview": return ( ); case "appearance": return ( ); case "models": return (
runProviderOAuth(provider, "login")} onSave={saveModelSettings} onMigrate={handleMigrateModelConfigurations} onBeginCreate={beginModelPresetCreation} onCancelCreate={cancelModelPresetCreation} onSelectConfiguration={() => { setModelPresetCreating(false); modelPresetBeforeCreateRef.current = null; }} onDeleteConfiguration={setModelPresetPendingDelete} /> setProviderForms((prev) => ({ ...prev, [provider]: { ...(prev[provider] ?? providerFormFromRow( settings.providers.find((row) => row.name === provider) ?? { name: provider, label: provider, configured: false, }, )), ...value, }, })) } onSaveProvider={saveProvider} onCreateCustomProvider={createCustomProvider} onProviderOAuthLogin={(provider) => runProviderOAuth(provider, "login")} onProviderOAuthLogout={(provider) => runProviderOAuth(provider, "logout")} imageProviderRestartPending={pendingRestartSections.image || pendingRestartSections.runtime} onRestart={restartViaSettingsSurface} isRestarting={isRestarting || hostEngineApplying} />
); case "image": return ( selectSection("models")} showBrandLogos={localPrefs.brandLogos} onRestart={restartViaSettingsSurface} isRestarting={isRestarting || hostEngineApplying} requiresRestartPending={pendingRestartSections.image} /> ); case "voice": return ( selectSection("models")} showBrandLogos={localPrefs.brandLogos} onRestart={restartViaSettingsSurface} isRestarting={isRestarting || hostEngineApplying} requiresRestartPending={pendingRestartSections.browser} /> ); case "browser": return ( setWebSearchKeyVisible((visible) => !visible)} onToggleKeyEditing={() => { setWebSearchKeyEditing((editing) => !editing); setWebSearchKeyVisible(false); setWebSearchForm((prev) => ({ ...prev, apiKey: "" })); }} onReset={resetWebSearchDraft} onSave={saveWebSearch} showBrandLogos={localPrefs.brandLogos} onRestart={restartViaSettingsSurface} isRestarting={isRestarting || hostEngineApplying} requiresRestartPending={pendingRestartSections.browser} olostepFeature={featureCatalog.find((feature) => feature.name === "olostep")} olostepInstalling={nanobotFeatureAction === "enable:olostep"} capabilityError={nanobotFeaturesError} /> ); case "channels": return ( { setNanobotFeaturesError(null); }} onRestart={restartViaSettingsSurface} isRestarting={isRestarting || hostEngineApplying} /> ); case "apps": return ( { setCliAppsMessage(null); setCliAppsError(null); setMcpMessage(null); setMcpError(null); }} onBackToChat={onBackToChat} onMcpFieldChange={(presetName, fieldName, value) => { setMcpFieldValues((prev) => ({ ...prev, [presetName]: { ...(prev[presetName] ?? {}), [fieldName]: value, }, })); }} onCustomMcpFormChange={setCustomMcpForm} onMcpConfigImportChange={setMcpConfigImport} onSaveCustomMcp={handleSaveCustomMcp} onImportMcpConfig={handleImportMcpConfig} onMcpToolsChange={handleMcpToolsChange} onRestart={restartViaSettingsSurface} isRestarting={isRestarting || hostEngineApplying} /> ); case "automations": return ( ); case "skills": return ; case "runtime": return ( feature.name === "langfuse")} capabilitiesLoading={nanobotFeaturesLoading} capabilityAction={nanobotFeatureAction} capabilityError={nanobotFeaturesError} onApiServiceAction={handleApiServiceAction} onInstallCapability={(name) => void installCapabilities([name])} /> ); case "advanced": return ( ); default: return null; } }; return (
{showSidebar ? ( ) : null} { if (!open) setModelPresetPendingDelete(null); }} onConfirm={handleDeleteModelConfiguration} /> { if (!xaiOAuthFlow) return; const opened = window.open(xaiOAuthFlow.authorization_url, "_blank", "noopener,noreferrer"); if (opened) opened.opener = null; }} onComplete={() => void completeXaiOAuth()} onClose={closeXaiOAuthFlow} /> { if (!open) setNanobotFeatureConfirm(null); }} onConfirm={(feature) => handleNanobotFeatureAction("enable", feature.name, true)} /> { if (!open) setAutomationPendingDelete(null); }} onConfirm={(job) => handleAutomationAction("delete", job)} /> { if (!open) setAutomationPendingEdit(null); }} onSave={handleAutomationEdit} />
{!showSidebar ? (

{t(`settings.nav.${activeSection}`, { defaultValue: standaloneSectionTitle(activeSection), })}

) : null} {loading ? (
{t("settings.status.loading")}
) : error && !settings ? ( {error} ) : settings ? (
{error ? (
{error}
) : null} {renderSection()}
) : null}
); } const SETTINGS_NAV_ITEMS: Array<{ key: SettingsSectionKey; icon: LucideIcon; fallback: string }> = [ { key: "overview", icon: Activity, fallback: "Overview" }, { key: "appearance", icon: Palette, fallback: "Appearance" }, { key: "models", icon: SlidersHorizontal, fallback: "Models" }, { key: "image", icon: ImageIcon, fallback: "Image" }, { key: "voice", icon: Mic, fallback: "Voice" }, { key: "browser", icon: Globe2, fallback: "Web" }, { key: "channels", icon: MessageCircle, fallback: "Channels" }, { key: "runtime", icon: Server, fallback: "System" }, { key: "advanced", icon: ShieldCheck, fallback: "Security" }, ]; function visibleWebuiDefaultAccessMode(mode: string | null | undefined): WebuiDefaultAccessMode { return mode === "full" ? "full" : "default"; } function standaloneSectionTitle(section: SettingsSectionKey): string { if (section === "apps") return "Apps"; if (section === "automations") return "Automations"; if (section === "skills") return "Skills"; return SETTINGS_NAV_ITEMS.find((item) => item.key === section)?.fallback ?? "Settings"; } function SettingsSidebar({ activeSection, onSelectSection, onBackToChat, onLogout, hostChromeInset, }: { activeSection: SettingsSectionKey; onSelectSection: (section: SettingsSectionKey) => void; onBackToChat: () => void; onLogout?: () => void; hostChromeInset?: boolean; }) { const { t } = useTranslation(); const activeItem = SETTINGS_NAV_ITEMS.find((item) => item.key === activeSection) ?? SETTINGS_NAV_ITEMS[0]; const ActiveIcon = activeItem.icon; const activeLabel = t(`settings.nav.${activeItem.key}`, { defaultValue: activeItem.fallback, }); return ( ); } function OverviewSettings({ settings, requiresRestart, onSelectSection, showBrandLogos, }: { settings: SettingsPayload; requiresRestart: boolean; onSelectSection: (section: SettingsSectionKey) => void; showBrandLogos: boolean; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const activePresetName = settings.agent.model_preset; const activePreset = activePresetName && activePresetName !== "default" ? settings.model_presets.find((preset) => preset.name === activePresetName)?.label ?? activePresetName : null; const activeProvider = settings.agent.resolved_provider ?? settings.agent.provider; const activeProviderConfigured = settingsProviderConfigured(settings, activeProvider); const activeProviderLabel = providerDisplayLabel(settings.providers, activeProvider); const activeModelValue = activeProviderConfigured ? settings.agent.model : tx("settings.values.notConfigured", "Not configured"); const activeModelCaption = activeProviderConfigured ? [activeProvider, activePreset].filter(Boolean).join(" · ") : activeProviderLabel || settings.agent.model ? [activeProviderLabel, settings.agent.model].filter(Boolean).join(" · ") : tx("settings.byok.noConfiguredProviders", "No configured providers"); const webStatus = settings.web.enable ? tx("settings.values.enabled", "Enabled") : tx("settings.values.disabled", "Disabled"); const webSearchProvider = settings.web_search.providers.find((provider) => provider.name === settings.web_search.provider) ?? settings.web_search.providers[0]; const webSearchProviderLabel = providerDisplayLabel( settings.web_search.providers, settings.web_search.provider, ); const webSearchCredentialStatus = webSearchProvider?.credential === "none" ? tx("settings.byok.webSearch.noCredentialRequired", "No key required") : webSearchProvider?.credential === "optional_api_key" ? settings.web_search.api_key_hint ? tx("settings.values.configured", "Configured") : tx("settings.byok.webSearch.noCredentialRequired", "No key required") : webSearchProvider?.credential === "base_url" ? settings.web_search.base_url ? tx("settings.values.configured", "Configured") : tx("settings.values.notConfigured", "Not configured") : settings.web_search.api_key_hint ? tx("settings.values.configured", "Configured") : tx("settings.values.notConfigured", "Not configured"); const webCaption = `${webSearchProviderLabel} · ${webSearchCredentialStatus}`; const imageStatus = settings.image_generation.enabled ? tx("settings.values.enabled", "Enabled") : tx("settings.values.disabled", "Disabled"); const imageCaption = `${providerDisplayLabel(settings.image_generation.providers, settings.image_generation.provider)} · ${ settings.image_generation.provider_configured ? tx("settings.values.configured", "Configured") : tx("settings.values.notConfigured", "Not configured") }`; const transcription = settings.transcription ?? DEFAULT_TRANSCRIPTION_SETTINGS; const voiceStatus = transcription.enabled ? tx("settings.values.enabled", "Enabled") : tx("settings.values.disabled", "Disabled"); const voiceCaption = `${providerDisplayLabel(transcription.providers, transcription.provider)} · ${ transcription.provider_configured ? tx("settings.values.configured", "Configured") : tx("settings.values.notConfigured", "Not configured") }`; const isNativeHost = (settings.surface ?? settings.runtime_surface) === "native"; const workspaceCaption = shortWorkspacePath(settings.runtime.workspace_path); const runtimeTitle = isNativeHost ? tx("settings.rows.engine", "Engine") : tx("settings.rows.gateway", "Gateway"); const runtimeValue = isNativeHost ? tx("settings.values.privateEngine", "Private engine") : `${settings.runtime.gateway_host}:${settings.runtime.gateway_port}`; const runtimeCaption = isNativeHost ? tx("settings.values.unixSocket", "Unix socket") : requiresRestart ? tx("settings.values.restartPending", "Restart pending") : tx("settings.values.ready", "Ready"); return (
{tx("settings.sections.ai", "AI")} onSelectSection("models")} />
{tx("settings.sections.capabilities", "Capabilities")} onSelectSection("browser")} /> onSelectSection("image")} /> onSelectSection("voice")} />
{tx("settings.sections.system", "System")} onSelectSection("runtime")} /> onSelectSection("runtime")} />
{tx("settings.sections.about", "About")}
); } function VersionCheckRow({ currentVersion }: { currentVersion?: string }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const { token } = useClient(); const [checking, setChecking] = useState(false); const [result, setResult] = useState< | { type: "up-to-date" } | { type: "update"; latestVersion: string; pypiUrl?: string } | { type: "error"; message: string } | null >(null); const handleCheck = async () => { setChecking(true); setResult(null); try { const res = await checkVersion(token); if (res.updateAvailable) { setResult({ type: "update", latestVersion: res.updateAvailable.latestVersion, pypiUrl: res.updateAvailable.pypiUrl, }); } else { setResult({ type: "up-to-date" }); } } catch (err) { setResult({ type: "error", message: (err as Error).message }); } finally { setChecking(false); } }; return (
{tx("settings.about.version", "Version")}
{currentVersion ? `v${currentVersion}` : "nanobot"}
{result?.type === "up-to-date" ? ( {tx("settings.about.upToDate", "You're up to date")} ) : null} {result?.type === "update" ? ( {t("settings.about.updateAvailable", { defaultValue: "Update available v{{version}}", version: result.latestVersion, })} {result.pypiUrl ? ( PyPI ) : null} ) : null} {result?.type === "error" ? ( {result.message} ) : null}
); } function AppearanceSettings({ theme, onToggleTheme, localPrefs, onChangeLocalPrefs, }: { theme: "light" | "dark"; onToggleTheme: () => void; localPrefs: LocalPreferences; onChangeLocalPrefs: Dispatch>; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); return (
{t("settings.sections.interface")}
{tx("settings.sections.localPreferences", "Local preferences")} onChangeLocalPrefs((prev) => ({ ...prev, density: density as LocalDensity })) } /> onChangeLocalPrefs((prev) => ({ ...prev, activityMode: activityMode as LocalActivityMode })) } /> onChangeLocalPrefs((prev) => ({ ...prev, fileEditDisplayMode: fileEditDisplayMode as FileEditDisplayMode, })) } /> onChangeLocalPrefs((prev) => ({ ...prev, codeWrap }))} ariaLabel={tx("settings.rows.codeWrap", "Code wrapping")} label={localPrefs.codeWrap ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")} /> onChangeLocalPrefs((prev) => ({ ...prev, brandLogos }))} ariaLabel={tx("settings.rows.brandLogos", "Brand logos")} label={localPrefs.brandLogos ? tx("settings.values.on", "On") : tx("settings.values.off", "Off")} />
); } function XaiOAuthLoginDialog({ flow, authorizationCode, completing, remoteBrowserAccess, onAuthorizationCodeChange, onOpenAuthorization, onComplete, onClose, }: { flow: ProviderOAuthAuthorizationRequired | null; authorizationCode: string; completing: boolean; remoteBrowserAccess: boolean; onAuthorizationCodeChange: (value: string) => void; onOpenAuthorization: () => void; onComplete: () => void; onClose: () => void; }) { const { t } = useTranslation(); return ( { if (!open) onClose(); }} >
{ event.preventDefault(); onComplete(); }} > xAI Grok {remoteBrowserAccess ? t("settings.oauth.remoteCodeHelp") : t("settings.oauth.localCodeHelp")}
onAuthorizationCodeChange(event.target.value)} placeholder={t("settings.oauth.authorizationCode")} aria-label={t("settings.oauth.authorizationCode")} autoComplete="off" spellCheck={false} />
); } function ModelPresetDeleteDialog({ preset, deleting, onOpenChange, onConfirm, }: { preset: SettingsPayload["model_presets"][number] | null; deleting: boolean; onOpenChange: (open: boolean) => void; onConfirm: () => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string, values?: Record) => t(key, { defaultValue: fallback, ...(values ?? {}) }); return ( {tx("settings.models.deletePresetTitle", "Delete model preset?")} {tx( "settings.models.deletePresetHelp", "This removes the preset “{{name}}”. Provider credentials are not affected.", { name: preset?.label ?? "" }, )} ); } function CapabilityInstallNotice({ title, description, installing = false, }: { title: string; description: string; installing?: boolean; }) { return (
{installing ? ( ) : ( )}

{title}

{description}

); } function ModelsSettings({ token, form, setForm, settings, dirty, creating, creatingSaving, callOrder, saving, orderSaving, migrationSaving, showBrandLogos, providerSaving, onChangeCallOrder, onProviderOAuthLogin, onSave, onMigrate, onBeginCreate, onCancelCreate, onSelectConfiguration, onDeleteConfiguration, }: { token: string; form: AgentSettingsDraft; setForm: Dispatch>; settings: SettingsPayload; dirty: boolean; creating: boolean; creatingSaving: boolean; callOrder: string[]; saving: boolean; orderSaving: boolean; migrationSaving: boolean; showBrandLogos: boolean; providerSaving: string | null; onChangeCallOrder: (order: string[]) => void; onProviderOAuthLogin: (provider: string) => void; onSave: () => void; onMigrate: () => void; onBeginCreate: () => void; onCancelCreate: () => void; onSelectConfiguration: () => void; onDeleteConfiguration: (preset: SettingsPayload["model_presets"][number]) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string, values?: Record) => t(key, { defaultValue: fallback, ...(values ?? {}) }); const [editorOpen, setEditorOpen] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false); const [draggedCallOrderIndex, setDraggedCallOrderIndex] = useState(null); const [dragOverCallOrderIndex, setDragOverCallOrderIndex] = useState(null); const namedPresets = settings.model_presets.filter((preset) => !preset.is_default); const namedPresetsByName = new Map(namedPresets.map((preset) => [preset.name, preset])); const unorderedPresets = namedPresets.filter((preset) => !callOrder.includes(preset.name)); const callOrderOccurrences = new Map(); const presetRows = [ ...callOrder.map((name, orderIndex) => { const occurrence = callOrderOccurrences.get(name) ?? 0; callOrderOccurrences.set(name, occurrence + 1); return { key: `ordered:${name}:${occurrence}`, name, orderIndex, preset: namedPresetsByName.get(name), }; }), ...unorderedPresets.map((preset) => ({ key: `disabled:${preset.name}`, name: preset.name, orderIndex: -1, preset, })), ]; const selectedPreset = namedPresetsByName.get(form.modelPreset) ?? null; useEffect(() => { setAdvancedOpen(false); }, [editorOpen, selectedPreset?.name]); const configuredProviders = settings.providers.filter((provider) => provider.configured); const selectedProvider = settings.providers.find((provider) => provider.name === form.provider); const selectableProviders = uniqueProviders([ ...configuredProviders, ...(selectedProvider ? [selectedProvider] : []), ]); const showAutoProvider = selectedPreset?.provider === "auto" || form.provider === "auto"; const providerOptions = showAutoProvider ? [{ name: "auto", label: tx("settings.values.auto", "Auto") }, ...selectableProviders] : selectableProviders; const providerValue = providerOptions.some((provider) => provider.name === form.provider) ? form.provider : ""; const selectedProviderNeedsSignIn = selectedProvider?.auth_type === "oauth" && !selectedProvider.configured; const selectedProviderSigningIn = providerSaving === selectedProvider?.name; const selectedProviderConfigured = settingsProviderConfigured( settings, form.provider, selectedPreset?.resolved_provider, ); const modelFieldsMissing = !form.model.trim() || !form.provider.trim() || !form.presetLabel.trim() || form.maxTokens <= 0 || form.temperature < 0 || form.temperature > 2; const selectedPresetReferenced = Boolean( selectedPreset && (settings.model_call_order ?? []).includes(selectedPreset.name), ); const callOrderBusy = orderSaving || saving; const selectPreset = (preset: SettingsPayload["model_presets"][number]) => { const toggleCurrentPreset = !creating && selectedPreset?.name === preset.name; onSelectConfiguration(); if (toggleCurrentPreset) { setEditorOpen((open) => !open); return; } setForm((prev) => ({ ...prev, modelPreset: preset.name, model: preset.model, provider: preset.provider, presetLabel: preset.label, maxTokens: preset.max_tokens, contextWindowTokens: normalizeContextWindowTokens(preset.context_window_tokens), temperature: preset.temperature, reasoningEffort: preset.reasoning_effort ?? "", })); setEditorOpen(true); }; const moveCallOrderItem = (index: number, offset: -1 | 1) => { if (callOrderBusy) return; const nextIndex = index + offset; if (nextIndex < 0 || nextIndex >= callOrder.length) return; const next = [...callOrder]; [next[index], next[nextIndex]] = [next[nextIndex], next[index]]; onChangeCallOrder(next); }; const removeCallOrderItem = (index: number) => { if (callOrderBusy || callOrder.length <= 1) return; onChangeCallOrder(callOrder.filter((_, itemIndex) => itemIndex !== index)); }; const dropCallOrderItem = (targetIndex: number) => { if ( callOrderBusy || draggedCallOrderIndex === null || draggedCallOrderIndex === targetIndex ) { setDraggedCallOrderIndex(null); setDragOverCallOrderIndex(null); return; } const next = [...callOrder]; const moved = next.splice(draggedCallOrderIndex, 1)[0]; if (!moved) { setDraggedCallOrderIndex(null); setDragOverCallOrderIndex(null); return; } next.splice(targetIndex, 0, moved); setDraggedCallOrderIndex(null); setDragOverCallOrderIndex(null); onChangeCallOrder(next); }; return (
{tx("settings.models.presets", "Model presets")} {!settings.model_call_order_editable ? (

{tx("settings.models.convertTitle", "Convert the current model setup")}

{tx( "settings.models.convertHelp", "Turn the existing primary and fallback models into presets so their order can be managed here.", )}

) : ( <>
{presetRows.map(({ key, name, orderIndex, preset }) => { const ordered = orderIndex >= 0; const provider = preset ? modelPresetProviderKey(preset, settings) : settings.agent.resolved_provider ?? settings.agent.provider; const presetConfigured = preset ? settingsProviderConfigured( settings, preset.provider, preset.resolved_provider, ) : true; const isDropTarget = ordered && dragOverCallOrderIndex === orderIndex && draggedCallOrderIndex !== orderIndex; const dropAfterTarget = isDropTarget && draggedCallOrderIndex !== null && draggedCallOrderIndex < orderIndex; return (
{ if (!ordered || callOrderBusy) { event.preventDefault(); return; } event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", name); setDraggedCallOrderIndex(orderIndex); setDragOverCallOrderIndex(orderIndex); }} onDragEnd={() => { setDraggedCallOrderIndex(null); setDragOverCallOrderIndex(null); }} onDragEnter={(event) => { if (ordered && draggedCallOrderIndex !== null) { event.preventDefault(); setDragOverCallOrderIndex(orderIndex); } }} onDragOver={(event) => { if (!ordered || draggedCallOrderIndex === null) return; event.preventDefault(); event.dataTransfer.dropEffect = "move"; }} onDrop={(event) => { if (!ordered) return; event.preventDefault(); dropCallOrderItem(orderIndex); }} onKeyDown={(event) => { if (event.currentTarget !== event.target) return; if (ordered && event.key === "ArrowUp") { event.preventDefault(); moveCallOrderItem(orderIndex, -1); } else if (ordered && event.key === "ArrowDown") { event.preventDefault(); moveCallOrderItem(orderIndex, 1); } else if ((event.key === "Enter" || event.key === " ") && preset) { event.preventDefault(); selectPreset(preset); } }} className={cn( "group relative flex min-h-[76px] select-none items-center gap-3 px-4 py-3 outline-none transition-[background-color,opacity] duration-150 sm:px-5", ordered && (callOrderBusy ? "cursor-wait" : "cursor-grab active:cursor-grabbing"), "hover:bg-muted/25", isDropTarget && !dropAfterTarget && "before:absolute before:inset-x-4 before:top-0 before:z-10 before:h-0.5 before:rounded-full before:bg-foreground sm:before:inset-x-5", isDropTarget && dropAfterTarget && "after:absolute after:inset-x-4 after:bottom-0 after:z-10 after:h-0.5 after:rounded-full after:bg-foreground sm:after:inset-x-5", ordered && draggedCallOrderIndex === orderIndex && "opacity-35", "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring", )} > {ordered ? ( ) : ( )}
); })}
{!creating ? ( ) : ( )} {orderSaving ? ( {tx("settings.actions.saving", "Saving...")} ) : null}
)} {editorOpen && (selectedPreset || creating) ? ( <>
{creating ? tx("settings.models.newPreset", "New model preset") : tx("settings.models.editPreset", "Edit preset")}
setForm((prev) => ({ ...prev, presetLabel: event.target.value })) } className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]" /> setForm((prev) => ({ ...prev, provider, model: provider === prev.provider ? prev.model : "", })) } /> {selectedProviderNeedsSignIn ? ( ) : null} setForm((prev) => ({ ...prev, model }))} /> {advancedOpen ? (
setForm((prev) => ({ ...prev, ...value }))} />
) : null}
{creating ? ( ) : selectedPreset ? ( ) : null}
) : null}
); } function ModelAdvancedFields({ maxTokens, contextWindowTokens, temperature, reasoningEffort, onChange, }: { maxTokens: number; contextWindowTokens: number; temperature: number; reasoningEffort: string; onChange: ( value: Partial< Pick< AgentSettingsDraft, "maxTokens" | "contextWindowTokens" | "temperature" | "reasoningEffort" > >, ) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const contextWindowOptions = Array.from( new Set([...CONTEXT_WINDOW_TOKEN_OPTIONS, contextWindowTokens]), ).sort((left, right) => left - right); return (
{tx("settings.rows.contextWindow", "Context window")} ({ value: String(tokens), label: formatModelContextWindow(tokens), }))} onChange={(value) => onChange({ contextWindowTokens: normalizeContextWindowTokens(Number(value)) }) } />
); } function ProviderAdvancedOptions({ fields, form, onChange, footer, }: { fields: ProviderAdvancedField[]; form: ProviderForm; onChange: (value: Partial) => void; footer?: ReactNode; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const enabled = new Set(fields); if (enabled.size === 0) return null; const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const thinkingStyleOptions = [ { value: "", label: tx("settings.values.default", "Default") }, { value: "thinking_type", label: "thinking_type" }, { value: "enable_thinking", label: "enable_thinking" }, { value: "reasoning_split", label: "reasoning_split" }, ]; return (
{open ? (
{enabled.has("api_type") ? ( ) : null} {enabled.has("thinking_style") ? ( ) : null} {enabled.has("proxy") ? ( ) : null} {enabled.has("region") ? ( ) : null} {enabled.has("profile") ? ( ) : null} {enabled.has("extra_headers") ? (