import { Checkbox, Picker, TextField, } from "@agent-native/toolkit/design-system"; import { Button as ToolkitButton } from "@agent-native/toolkit/ui/button"; import { IconChevronDown, IconChevronRight, IconCheck, IconExternalLink, IconBrain, IconHierarchy2, IconBrowser, IconGitBranch, IconCloud, IconDatabase, IconShield, IconPlugConnected, IconTopologyRing2, IconLoader2, IconUpload, IconCoin, IconMail, IconKey, IconMicrophone, IconEyeOff, IconBolt, IconGauge, IconApps, IconUsersGroup, IconTool, } from "@tabler/icons-react"; import React, { Suspense, lazy, useState, useEffect, useCallback, useMemo, useRef, } from "react"; import { Link, Navigate } from "react-router"; import { PROVIDER_ENV_PLACEHOLDERS } from "../../agent/engine/provider-env-vars.js"; import { saveAgentEngineProviderSettings } from "../agent-engine-key.js"; import { agentNativePath } from "../api-path.js"; import { BuilderBMark } from "../builder-mark.js"; import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip.js"; import { useT } from "../i18n.js"; import { TeamPage } from "../org/TeamPage.js"; import { BuilderConnectCard } from "../setup-connections/BuilderConnectCard.js"; import { callAction } from "../use-action.js"; import { useDevMode } from "../use-dev-mode.js"; import { cn } from "../utils.js"; import { AGENT_SETTINGS_SECTIONS, ALL_SETTINGS_SECTIONS, CONNECTION_SETTINGS_SECTIONS, WORKSPACE_SETTINGS_SECTIONS, getAgentSettingsSearchTabs, type SettingsSectionId, } from "./agent-settings-search.js"; import { AgentsSection } from "./AgentsSection.js"; import { AutomationsSection } from "./AutomationsSection.js"; import { DemoModeSection } from "./DemoModeSection.js"; import { ExtensionsSettingsContent } from "./ExtensionsSettingsContent.js"; import { SecretsSection } from "./SecretsSection.js"; import { SettingsSection, SettingsSurfaceProvider, useSettingsSurface, type SettingsSurface, } from "./SettingsSection.js"; import type { SettingsTabItem } from "./SettingsTabsPage.js"; import { UsageSection } from "./UsageSection.js"; import { type BuilderConnectFlow, useBuilderConnectFlow, useBuilderStatus, } from "./useBuilderStatus.js"; import { settingsSectionDomId, useSettingsPanelController, } from "./useSettingsPanelController.js"; import { VoiceTranscriptionSection } from "./VoiceTranscriptionSection.js"; const Button = React.forwardRef< HTMLButtonElement, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); Button.displayName = "SettingsPrimitiveButton"; const IntegrationsPanel = lazy(() => import("../integrations/IntegrationsPanel.js").then((m) => ({ default: m.IntegrationsPanel, })), ); // ─── Shared helpers ───────────────────────────────────────────────────────── function SettingsSkeleton({ lines = 3 }: { lines?: number }) { return (
{Array.from({ length: lines }, (_, i) => (
{i < 2 && (
)}
))}
); } interface SettingsSelectOption { value: string; label: string; description?: string; } const CONTROL_STYLE = { fontSize: 12, lineHeight: 1, } satisfies React.CSSProperties; const CONTROL_STYLE_PAGE = { fontSize: 14, lineHeight: 1.2, } satisfies React.CSSProperties; // Surface-aware class helpers so section bodies (shared with the compact // sidebar) read as roomy, shadcn-style forms on the full settings page while // staying dense in the sidebar. function fieldLabelClass(isPage: boolean): string { return cn("font-medium text-foreground", isPage ? "text-sm" : "text-[12px]"); } // Secondary label / row-title size (e.g. "This app", provider names). function subTextClass(isPage: boolean): string { return isPage ? "text-sm" : "text-[11px]"; } // Helper / hint / status note size. function noteTextClass(isPage: boolean): string { return isPage ? "text-xs" : "text-[10px]"; } function textInputClass(isPage: boolean): string { return cn( "flex w-full rounded-md border border-border bg-background text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-accent placeholder:text-muted-foreground/50", isPage ? "h-10 px-3 text-sm" : "h-9 px-3 text-[12px]", ); } function pillButtonClass( isPage: boolean, tone: "solid" | "outline" | "ghost" = "outline", ): string { const base = cn( "inline-flex items-center justify-center gap-1 rounded-md font-medium transition-colors disabled:opacity-40", isPage ? "px-3 py-1.5 text-sm" : "px-2.5 py-1 text-[10px]", ); if (tone === "solid") { return cn(base, "bg-accent text-foreground hover:bg-accent/80"); } if (tone === "ghost") { return cn( base, "text-muted-foreground hover:bg-accent/40 hover:text-foreground", ); } return cn(base, "border border-border text-foreground hover:bg-accent/40"); } function SettingsSelect({ label, labelAdornment, value, options, onValueChange, disabled = false, }: { label: string; labelAdornment?: React.ReactNode; value: string; options: SettingsSelectOption[]; onValueChange: (value: string) => void; disabled?: boolean; }) { const isPage = useSettingsSurface() === "page"; const controlStyle = isPage ? CONTROL_STYLE_PAGE : CONTROL_STYLE; return (

{label}

{labelAdornment}
({ value: option.value, label: option.label, description: option.description, textValue: option.label, }))} value={value} onChange={(next) => { if (next != null) onValueChange(String(next)); }} disabled={disabled} aria-label={label} placeholder={value} style={controlStyle} className={cn( "w-full text-start text-foreground", isPage ? "text-sm" : "text-[12px]", )} />
); } // ─── Disconnect button for the Builder card's connected state ─────────────── // // Two-step confirmation: first click arms the button ("Confirm?"), second // click actually disconnects. Arm auto-reverts after 4s of idle so a user // who wandered off doesn't come back to a disconnect waiting for them. // // Hits /_agent-native/builder/disconnect which removes request-scoped // Builder credentials from app_secrets. Deployment env credentials are left // alone and remain as fallback. On success we dispatch // `agent-engine:configured-changed` so dependent cards refresh inline. function DisconnectBuilderButton() { const { status } = useBuilderStatus(); const [phase, setPhase] = useState<"idle" | "armed" | "busy">("idle"); const [err, setErr] = useState(null); const armedTimerRef = useRef | null>(null); const clearArmedTimer = useCallback(() => { if (armedTimerRef.current) { clearTimeout(armedTimerRef.current); armedTimerRef.current = null; } }, []); useEffect(() => { return () => clearArmedTimer(); }, [clearArmedTimer]); const performDisconnect = useCallback(async () => { setPhase("busy"); setErr(null); clearArmedTimer(); try { const res = await fetch( agentNativePath("/_agent-native/builder/disconnect"), { method: "POST", headers: { "Content-Type": "application/json" }, }, ); // Parse defensively — a nitro 404 fallback returns HTML, not JSON, // and res.json() on that would throw. const text = await res.text(); let body: { ok?: boolean; error?: string; warnings?: Record; } = {}; if (text) { try { body = JSON.parse(text); } catch { // Non-JSON response — likely a 404/HTML fallback. } } if (!res.ok) { throw new Error( body.error || `Failed (${res.status}). Is your dev server up to date?`, ); } if (body.ok !== true) { throw new Error(body.error || "Disconnect didn't confirm ok"); } if (body.warnings && Object.keys(body.warnings).length > 0) { // Disconnect flag persisted (we only reach here when ok:true), so // the user IS disconnected — but some ancillary cleanup failed. // Log so it's visible during dev; don't block the success path. console.warn( "[builder-disconnect] completed with warnings:", body.warnings, ); } window.dispatchEvent(new CustomEvent("agent-engine:configured-changed")); setPhase("idle"); } catch (e) { setErr(e instanceof Error ? e.message : "Disconnect failed"); setPhase("idle"); } }, [clearArmedTimer]); const handleDisconnectClick = useCallback(() => { if (phase === "busy") return; if (phase === "idle") { // First click — arm the button. Auto-revert after 4s to avoid a // stale "confirm" state someone else could hit by accident. setPhase("armed"); setErr(null); clearArmedTimer(); armedTimerRef.current = setTimeout(() => { setPhase("idle"); armedTimerRef.current = null; }, 4000); return; } // phase === "armed" — user confirmed, actually disconnect. void performDisconnect(); }, [phase, performDisconnect, clearArmedTimer]); const handleCancel = useCallback(() => { clearArmedTimer(); setPhase("idle"); }, [clearArmedTimer]); // When only the deploy fallback is active there is nothing request-scoped // for this button to remove. The early return MUST come after every hook // above to satisfy rules-of-hooks. if (status?.credentialSource === "env") return null; if (phase === "armed") { return ( <> ); } return ( <> {err && {err}} ); } // ─── "Connect Builder.io" card (shared across all sections) ───────────────── function UseBuilderCard({ builderFlow, connectUrl, connected, orgName, envManaged, credentialSource, trackingSource = "settings_panel_builder_card", trackingFlow = "connect_llm", label = "Connect Builder.io", subtitle = "Free credits to start — no API key needed.", dim, }: { builderFlow: BuilderConnectFlow; connectUrl?: string; connected: boolean; orgName?: string; envManaged?: boolean; credentialSource?: "user" | "org" | "workspace" | "env"; trackingSource?: string; trackingFlow?: string; label?: string; subtitle?: string; dim?: boolean; }) { const isPage = useSettingsSurface() === "page"; const effectiveConnected = connected || builderFlow.configured; const effectiveOrgName = builderFlow.orgName ?? orgName; const bgClass = dim ? "" : "bg-accent/30"; const titleCls = isPage ? "text-sm" : "text-[11px]"; const bodyCls = isPage ? "text-xs" : "text-[10px]"; if (effectiveConnected) { return (
Builder.io
Connected
{effectiveOrgName && (

{effectiveOrgName}

)} {envManaged ? (

{credentialSource === "env" ? "Deployment fallback is available. Connect your own account to override it." : "Using your connected Builder account. Deployment fallback is still available."}

) : null} {connectUrl || credentialSource !== "env" ? (
{connectUrl && ( )} {credentialSource !== "env" ? : null}
) : null}
); } if (!connectUrl) return null; return ( ); } // ─── Manual setup card ────────────────────────────────────────────────────── function ManualSetupCard({ id, hint, docsUrl, docsLabel = "Read the docs", children, dim, sourceBadge, }: { id?: string; hint?: string; docsUrl?: string; docsLabel?: string; children?: React.ReactNode; dim?: boolean; /** Optional "Connected via X" badge shown in the header row. */ sourceBadge?: string; }) { const isPage = useSettingsSurface() === "page"; const titleCls = isPage ? "text-sm" : "text-[11px]"; const bodyCls = isPage ? "text-xs" : "text-[10px]"; return (
Set up manually
{sourceBadge ? ( {sourceBadge} ) : null}
{hint && (

{hint}

)} {children} {docsUrl && ( {docsLabel} )}
); } // ─── LLM helpers ──────────────────────────────────────────────────────────── function friendlyModelName(model: string): string { if (model === "z-ai/glm-5.2") return "GLM 5.2"; const claude = model.match( /^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-\d{8,})?$/, ); if (claude) { const tier = claude[1][0].toUpperCase() + claude[1].slice(1); return `${tier} ${claude[2]}${claude[3] ? `.${claude[3]}` : ""}`; } if (model.startsWith("gpt-")) { const rest = model.slice(4); const gpt = rest.match(/^(\d+)[.-](\d+)(?:[.-](.+))?$/); if (gpt) { const suffix = gpt[3] ? ` ${gpt[3] .split("-") .map((part) => part[0].toUpperCase() + part.slice(1)) .join(" ")}` : ""; return `GPT-${gpt[1]}.${gpt[2]}${suffix}`; } return `GPT-${rest}`; } if (/^o\d/.test(model)) return model; const geminiVersioned = model.match( /^gemini-(\d+)-(\d+)-(.+?)(?:-preview)?$/, ); if (geminiVersioned) { const variant = geminiVersioned[3] .split("-") .map((part) => part[0].toUpperCase() + part.slice(1)) .join(" "); return `Gemini ${geminiVersioned[1]}.${geminiVersioned[2]} ${variant}`; } const gemini = model.match(/^gemini-(.+?)(?:-preview)?$/); if (gemini) { const parts = gemini[1] .split("-") .map((s) => s[0].toUpperCase() + s.slice(1)) .join(" "); return `Gemini ${parts}${model.endsWith("-preview") ? " (preview)" : ""}`; } return model; } type SettingsStatus = { engine: string; source: "env" | "settings"; envVar: string | null; } | null; function computeSourceBadge(args: { settingsConfigured: boolean; settingsStatus: SettingsStatus; envConfigured: boolean; envVar: string | undefined; builderConnected: boolean; }): string | undefined { const { settingsConfigured, settingsStatus } = args; if (args.builderConnected) return "Connected via Builder"; if (settingsConfigured) { if (settingsStatus?.source === "env") { return `Connected via ${settingsStatus.envVar ?? args.envVar ?? "env"}`; } return "Connected via template (server-side)"; } if (args.envConfigured) return `Connected via ${args.envVar ?? "env"}`; return undefined; } function latestModelsOnly(models: string[]): string[] { const seen = new Set(); return models.filter((m) => { const claude = m.match(/^claude-(opus|sonnet|haiku)-/); if (claude) { if (seen.has(claude[1])) return false; seen.add(claude[1]); return true; } const gemini = m.match(/^gemini-(\d+(?:\.\d+)?)-(.+?)(?:-preview)?$/); if (gemini) { const family = gemini[2]; if (seen.has(`gemini-${family}`)) return false; seen.add(`gemini-${family}`); return true; } return true; }); } export function AppDefaultModelField({ engine, models, value, defaultModel, disabled, onValueChange, onEnter, }: { engine: string; models: string[]; value: string; defaultModel?: string; disabled?: boolean; onValueChange: (value: string) => void; onEnter?: () => void; }) { const isPage = useSettingsSurface() === "page"; const modelOptions: SettingsSelectOption[] = latestModelsOnly(models).map( (model) => ({ value: model, label: friendlyModelName(model) }), ); // Builder models are a closed catalog (and are validated server-side), so a // real select keeps every available model visible even when one is already // selected. Native datalists filter against the current input value, which // made this field appear to contain only the active model. if (engine === "builder" && modelOptions.length > 0) { return ( ); } return (

Model

{ if (event.key === "Enter") onEnter?.(); }} placeholder={defaultModel ?? "model-id"} autoComplete="off" aria-label="Model" className={cn( "w-full disabled:opacity-60", isPage ? "text-sm" : "text-[12px]", )} style={isPage ? CONTROL_STYLE_PAGE : CONTROL_STYLE} /> {modelOptions.length > 0 && ( {modelOptions.map((option) => ( )}
); } // ─── LLM Section ──────────────────────────────────────────────────────────── interface EngineInfo { name: string; label: string; description: string; defaultModel: string; supportedModels: string[]; requiredEnvVars: string[]; installPackage?: string; packageInstalled?: boolean; } const PROVIDER_DOCS: Record = { anthropic: "https://console.anthropic.com/settings/keys", "ai-sdk:anthropic": "https://console.anthropic.com/settings/keys", "ai-sdk:openai": "https://platform.openai.com/api-keys", "ai-sdk:google": "https://aistudio.google.com/apikey", "ai-sdk:openrouter": "https://openrouter.ai/keys", "ai-sdk:groq": "https://console.groq.com/keys", "ai-sdk:mistral": "https://console.mistral.ai/api-keys/", "ai-sdk:cohere": "https://dashboard.cohere.com/api-keys", }; function LLMSectionInner({ builderFlow, builderLoading, connectUrl, connected, orgName, envManaged, credentialSource, open, onToggle, }: { builderFlow: BuilderConnectFlow; builderLoading?: boolean; connectUrl?: string; connected: boolean; orgName?: string; envManaged?: boolean; credentialSource?: "user" | "org" | "workspace" | "env"; open?: boolean; onToggle?: () => void; }) { const isPage = useSettingsSurface() === "page"; const t = useT(); const [envKeys, setEnvKeys] = useState< Array<{ key: string; configured: boolean }> >([]); const [apiKey, setApiKey] = useState(""); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [engines, setEngines] = useState([]); const [currentEngine, setCurrentEngine] = useState("anthropic"); const [currentModel, setCurrentModel] = useState(""); const [selectedEngine, setSelectedEngine] = useState("anthropic"); const [selectedModel, setSelectedModel] = useState(""); const [baseUrl, setBaseUrl] = useState(""); const [baseUrlConfigured, setBaseUrlConfigured] = useState(false); const [clearBaseUrl, setClearBaseUrl] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false); const [manualSetupOpen, setManualSetupOpen] = useState(false); const [applyNote, setApplyNote] = useState(false); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState< | { ok: true; latencyMs: number; model: string } | { ok: false; error: string } | null >(null); const [settingsStatus, setSettingsStatus] = useState(null); const [disconnectError, setDisconnectError] = useState(null); const [envLoaded, setEnvLoaded] = useState(false); const [enginesLoaded, setEnginesLoaded] = useState(false); const [statusLoaded, setStatusLoaded] = useState(false); const initialLoading = !envLoaded || !enginesLoaded || !statusLoaded || !!builderLoading; useEffect(() => { fetch(agentNativePath("/_agent-native/env-status")) .then((r) => (r.ok ? r.json() : [])) .then(setEnvKeys) .catch(() => {}) .finally(() => setEnvLoaded(true)); }, [saved]); const notifyConfigChanged = useCallback(() => { window.dispatchEvent(new CustomEvent("agent-engine:configured-changed")); }, []); const refreshSettingsStatus = useCallback(() => { fetch(agentNativePath("/_agent-native/agent-engine/status")) .then((r) => (r.ok ? r.json() : null)) .then((data) => { setBaseUrlConfigured(Boolean(data?.openAiBaseUrlConfigured)); if ( data?.configured && typeof data.engine === "string" && (data.source === "env" || data.source === "settings") ) { setSettingsStatus({ engine: data.engine, source: data.source, envVar: typeof data.envVar === "string" ? data.envVar : null, }); } else { setSettingsStatus(null); } }) .catch(() => {}) .finally(() => setStatusLoaded(true)); }, []); useEffect(() => { refreshSettingsStatus(); }, [refreshSettingsStatus]); useEffect(() => { callAction("manage-agent-engine" as any, { action: "list" } as any) .then((data) => { if (!data) return; const engineData = data as { engines?: EngineInfo[]; current?: { engine?: string; model?: string }; }; setEngines(engineData.engines ?? []); const cur = engineData.current ?? {}; setCurrentEngine(cur.engine ?? "anthropic"); setCurrentModel(cur.model ?? ""); setSelectedEngine(cur.engine ?? "anthropic"); setSelectedModel(cur.model ?? ""); }) .catch(() => {}) .finally(() => setEnginesLoaded(true)); }, []); const selectedEngineInfo = engines.find((e) => e.name === selectedEngine); const envVar = selectedEngineInfo?.requiredEnvVars?.[0]; const selectedEnginePackageInstalled = selectedEngineInfo?.packageInstalled !== false; const envConfigured = envVar ? (envKeys.find((k) => k.key === envVar)?.configured ?? false) : false; const settingsConfigured = settingsStatus != null && settingsStatus.engine === currentEngine; const builderConnected = connected || builderFlow.configured; const anyKeyConfigured = builderConnected || (selectedEnginePackageInstalled && (envConfigured || settingsConfigured)); const sourceBadge = computeSourceBadge({ settingsConfigured, settingsStatus, envConfigured, envVar, builderConnected, }); const manualSetupHint = selectedEngine === "ai-sdk:openrouter" ? "Provide an OpenRouter key to use OpenRouter models like GLM 5.2." : "Choose your AI provider and model."; const engineChanged = selectedEngine !== currentEngine || selectedModel !== currentModel; const isOpenAiEngine = selectedEngine === "ai-sdk:openai"; const endpointChanged = isOpenAiEngine && (!!baseUrl.trim() || clearBaseUrl); const providerSettingsChanged = !!apiKey.trim() || endpointChanged; // Hide the Anthropic-via-AI-SDK alias (redundant with the native entry) // and Ollama (no API key to set here). The currently-selected engine is // always kept so a stale setting doesn't vanish from the picker. const providerOptions: SettingsSelectOption[] = engines .filter( (e) => e.name === selectedEngine || (e.name !== "ai-sdk:anthropic" && e.name !== "ai-sdk:ollama"), ) .map((e) => ({ value: e.name, label: e.label })); const modelOptions: SettingsSelectOption[] = latestModelsOnly( selectedEngineInfo?.supportedModels ?? [], ).map((m) => ({ value: m, label: friendlyModelName(m) })); const handleSave = async () => { if (!providerSettingsChanged || !envVar) return; setSaving(true); try { const nextBaseUrl = isOpenAiEngine ? baseUrl.trim() : ""; await saveAgentEngineProviderSettings({ key: envVar, ...(apiKey.trim() ? { apiKey } : {}), ...(nextBaseUrl ? { baseUrl: nextBaseUrl } : {}), ...(isOpenAiEngine && clearBaseUrl ? { clearBaseUrl: true } : {}), }); setSaved(true); setApiKey(""); setBaseUrl(""); setClearBaseUrl(false); if (nextBaseUrl) setBaseUrlConfigured(true); if (clearBaseUrl) setBaseUrlConfigured(false); refreshSettingsStatus(); notifyConfigChanged(); setTimeout(() => setSaved(false), 2000); } finally { setSaving(false); } }; const handleDisconnect = async () => { setDisconnectError(null); try { const res = await fetch( agentNativePath("/_agent-native/agent-engine/disconnect"), { method: "POST", }, ); if (res.ok) { setTestResult(null); setApplyNote(false); refreshSettingsStatus(); notifyConfigChanged(); return; } const body = (await res.json().catch(() => null)) as { error?: string; } | null; setDisconnectError( body?.error ?? (res.status === 401 ? "You must be signed in to disconnect." : `Disconnect failed (HTTP ${res.status})`), ); } catch (err) { setDisconnectError(err instanceof Error ? err.message : String(err)); } }; const handleTest = async () => { setTesting(true); setTestResult(null); try { const data = await callAction( "manage-agent-engine" as any, { action: "test", engine: selectedEngine, model: selectedModel || selectedEngineInfo?.defaultModel, } as any, ); // Older action paths wrapped tool output in { result }. Accept either // shape while the action route normalizes JSON-string script output. const parsed = typeof data === "string" ? JSON.parse(data) : typeof data?.result === "string" ? JSON.parse(data.result) : data; if (parsed?.ok) { setTestResult({ ok: true, latencyMs: parsed.latencyMs ?? 0, model: parsed.model ?? selectedModel, }); } else { setTestResult({ ok: false, error: parsed?.error ?? "Test failed (no error message)", }); } } catch (err) { setTestResult({ ok: false, error: err instanceof Error ? err.message : String(err), }); } finally { setTesting(false); } }; const handleApply = async () => { try { const res = await fetch( agentNativePath("/_agent-native/actions/manage-agent-engine"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "set", engine: selectedEngine, model: selectedModel, }), }, ); if (res.ok) { setCurrentEngine(selectedEngine); setCurrentModel(selectedModel); setApplyNote(true); refreshSettingsStatus(); notifyConfigChanged(); setTimeout(() => setApplyNote(false), 4000); } } catch {} }; return ( } title="LLM" subtitle="Connect any major LLM — Claude, GPT, Gemini, and more." required connected={initialLoading ? undefined : anyKeyConfigured} open={open} onToggle={onToggle} > {initialLoading ? ( ) : (
{builderConnected && ( )} {(!builderConnected || manualSetupOpen) && (
{ setSelectedEngine(val); const info = engines.find((e) => e.name === val); setSelectedModel(info?.defaultModel ?? ""); setApiKey(""); setBaseUrl(""); setClearBaseUrl(false); setAdvancedOpen(false); }} /> {/* Free-form input so OpenRouter/Ollama custom model IDs can be typed — the registry's supportedModels is only suggestions. */}

Model

setSelectedModel(e.target.value)} placeholder={ selectedEngineInfo?.defaultModel ?? "e.g. model-id" } spellCheck={false} autoComplete="off" className={textInputClass(isPage)} style={isPage ? CONTROL_STYLE_PAGE : CONTROL_STYLE} /> {modelOptions.length > 0 && ( {modelOptions.map((opt) => ( )}
{isOpenAiEngine && (
{advancedOpen && (

Endpoint URL

{baseUrlConfigured ? "Configured" : "Optional"}
{ setBaseUrl(e.target.value); if (e.target.value.trim()) setClearBaseUrl(false); }} onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} placeholder={ baseUrlConfigured ? "Leave blank to keep current endpoint" : "https://gateway.example/v1" } disabled={clearBaseUrl} spellCheck={false} autoComplete="off" className="flex h-9 w-full rounded-md border border-border bg-background px-3 text-[12px] text-foreground outline-none transition-colors hover:bg-accent/40 focus:ring-1 focus:ring-accent disabled:opacity-50 placeholder:text-muted-foreground/50" style={CONTROL_STYLE} />

Use for LiteLLM or another OpenAI-compatible chat gateway. Leave blank for OpenAI.

{baseUrlConfigured && ( )} {envVar && envConfigured && endpointChanged && ( )}
)}
)} {envVar && envConfigured ? (
{envVar} configured
) : envVar ? (
setApiKey(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }} placeholder={PROVIDER_ENV_PLACEHOLDERS[envVar] ?? "..."} className={cn(textInputClass(isPage), "flex-1")} style={isPage ? CONTROL_STYLE_PAGE : undefined} />
) : null}
{PROVIDER_DOCS[selectedEngine] ? ( Get an API key ) : null} {engineChanged && ( )} {settingsStatus != null && ( Clear the saved engine — the app will fall back to the default until you re-apply. )}
{testResult && testResult.ok && (

Test passed — {testResult.latencyMs}ms

)} {testResult && testResult.ok === false && (

Test failed: {testResult.error}

)} {disconnectError && (

Disconnect failed: {disconnectError}

)} {applyNote && (

Changes take effect on next conversation

)}
)}
)}
); } // ─── App Default Model Section ────────────────────────────────────────────── interface AppModelDefaultEngine extends EngineInfo { configured: boolean; } interface AppModelDefaultsResponse { appId: string; engine: string | null; model: string | null; scope: "org" | "user" | "default"; source: "org" | "user" | "default"; canUpdate: boolean; orgId?: string | null; orgName?: string | null; role?: string | null; engines: AppModelDefaultEngine[]; } function friendlyAppName(appId: string): string { return appId .split("-") .filter(Boolean) .map((part) => part[0]?.toUpperCase() + part.slice(1)) .join(" "); } function AppModelDefaultsSectionInner({ open, onToggle, }: { open?: boolean; onToggle?: () => void; }) { const isPage = useSettingsSurface() === "page"; const [settings, setSettings] = useState( null, ); const [selectedEngine, setSelectedEngine] = useState(""); const [selectedModel, setSelectedModel] = useState(""); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); const load = useCallback(() => { let cancelled = false; setLoading(true); fetch(agentNativePath("/_agent-native/agent-model-defaults")) .then((r) => (r.ok ? r.json() : null)) .then((data: AppModelDefaultsResponse | null) => { if (cancelled || !data) return; setSettings(data); const firstConfigured = data.engines.find((engine) => engine.configured) ?? data.engines[0]; const nextEngine = data.engine ?? firstConfigured?.name ?? ""; const nextEngineInfo = data.engines.find((engine) => engine.name === nextEngine) ?? firstConfigured; setSelectedEngine(nextEngine); setSelectedModel(data.model ?? nextEngineInfo?.defaultModel ?? ""); }) .catch(() => {}) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); useEffect(() => load(), [load]); const selectedEngineInfo = settings?.engines.find((engine) => engine.name === selectedEngine) ?? null; const engineOptions: SettingsSelectOption[] = (settings?.engines ?? []) .filter( (engine) => engine.name === selectedEngine || (engine.name !== "ai-sdk:anthropic" && engine.name !== "ai-sdk:ollama"), ) .map((engine) => ({ value: engine.name, label: engine.name === "builder" ? "Builder.io Gateway" : engine.label || engine.name, description: engine.configured ? "Configured for this workspace" : engine.packageInstalled === false ? `Install ${engine.installPackage ?? "the provider packages"} to use this provider` : "Credentials not detected yet", })); const hasPendingChange = !!settings && settings.canUpdate && !!selectedEngine && !!selectedModel.trim() && (selectedEngine !== settings.engine || selectedModel.trim() !== settings.model); const hasAppDefault = settings?.source !== "default"; const scopeLabel = settings?.scope === "org" ? settings.orgName ? `${settings.orgName} organization` : "organization" : "your account"; const notifyChanged = () => { window.dispatchEvent(new CustomEvent("agent-engine:configured-changed")); }; const save = async () => { if (!hasPendingChange) return; setSaving(true); setSaved(false); setError(null); try { const res = await fetch( agentNativePath("/_agent-native/agent-model-defaults"), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ engine: selectedEngine, model: selectedModel.trim(), }), }, ); const body = await res.json().catch(() => ({})); if (!res.ok) throw new Error(body?.error ?? `Save failed (${res.status})`); const next = body as AppModelDefaultsResponse; setSettings(next); setSelectedEngine(next.engine ?? selectedEngine); setSelectedModel(next.model ?? selectedModel.trim()); setSaved(true); notifyChanged(); setTimeout(() => setSaved(false), 2000); } catch (err) { setError(err instanceof Error ? err.message : "Save failed"); } finally { setSaving(false); } }; const reset = async () => { if (!settings?.canUpdate || !hasAppDefault) return; setSaving(true); setSaved(false); setError(null); try { const res = await fetch( agentNativePath("/_agent-native/agent-model-defaults"), { method: "DELETE" }, ); const body = await res.json().catch(() => ({})); if (!res.ok) throw new Error(body?.error ?? `Reset failed (${res.status})`); const next = body as AppModelDefaultsResponse; setSettings(next); const fallback = next.engines.find((engine) => engine.configured); setSelectedEngine(next.engine ?? fallback?.name ?? selectedEngine); setSelectedModel(next.model ?? fallback?.defaultModel ?? selectedModel); notifyChanged(); } catch (err) { setError(err instanceof Error ? err.message : "Reset failed"); } finally { setSaving(false); } }; return ( } title="App Default Model" subtitle="Choose the default model for this app/template when no one-off composer model is selected." connected={loading ? undefined : hasAppDefault} open={open} onToggle={onToggle} > {loading ? ( ) : settings ? (

{friendlyAppName(settings.appId) || "This app"}

{hasAppDefault ? `Applies to ${scopeLabel}.` : "Using the global LLM default."}

{settings.source}
{ setSelectedEngine(value); const info = settings.engines.find( (engine) => engine.name === value, ); setSelectedModel(info?.defaultModel ?? ""); setError(null); }} /> { setSelectedModel(value); setError(null); }} onEnter={() => { if (hasPendingChange) void save(); }} />
{!settings.canUpdate && (

Only organization owners and admins can change app model defaults.

)} {selectedEngineInfo?.packageInstalled === false ? (

This app does not include the optional runtime packages for this provider.

) : selectedEngineInfo && !selectedEngineInfo.configured ? (

Credentials for this provider were not detected; runtime will fall back if the model cannot be used.

) : null} {error && (

{error}

)}
) : (

App model defaults are unavailable.

)}
); } // ─── Email Section ────────────────────────────────────────────────────────── function EmailSectionInner({ open, onToggle, }: { open?: boolean; onToggle?: () => void; }) { const isPage = useSettingsSurface() === "page"; const emailInputCls = cn(textInputClass(isPage), "flex-1"); const emailBtnCls = pillButtonClass(isPage, "solid"); const emailIconSize = isPage ? 14 : 10; const [envKeys, setEnvKeys] = useState< Array<{ key: string; configured: boolean }> >([]); const [resendKey, setResendKey] = useState(""); const [sendgridKey, setSendgridKey] = useState(""); const [fromAddr, setFromAddr] = useState(""); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [emailProvider, setEmailProvider] = useState<"resend" | "sendgrid">( "resend", ); const [envLoaded, setEnvLoaded] = useState(false); useEffect(() => { fetch(agentNativePath("/_agent-native/env-status")) .then((r) => (r.ok ? r.json() : [])) .then(setEnvKeys) .catch(() => {}) .finally(() => setEnvLoaded(true)); }, [saved]); const resendConfigured = envKeys.find((k) => k.key === "RESEND_API_KEY")?.configured ?? false; const sendgridConfigured = envKeys.find((k) => k.key === "SENDGRID_API_KEY")?.configured ?? false; const fromConfigured = envKeys.find((k) => k.key === "EMAIL_FROM")?.configured ?? false; const anyConfigured = resendConfigured || sendgridConfigured; useEffect(() => { if (sendgridConfigured && !resendConfigured) { setEmailProvider("sendgrid"); } }, [resendConfigured, sendgridConfigured]); const save = async (vars: Array<{ key: string; value: string }>) => { setSaving(true); try { const res = await fetch(agentNativePath("/_agent-native/env-vars"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vars }), }); if (res.ok) { setSaved(true); setResendKey(""); setSendgridKey(""); setFromAddr(""); setTimeout(() => setSaved(false), 2000); } } finally { setSaving(false); } }; const saveResend = () => { const vars: Array<{ key: string; value: string }> = []; if (resendKey.trim()) vars.push({ key: "RESEND_API_KEY", value: resendKey.trim() }); if (fromAddr.trim()) vars.push({ key: "EMAIL_FROM", value: fromAddr.trim() }); if (vars.length) save(vars); }; const saveSendgrid = () => { const vars: Array<{ key: string; value: string }> = []; if (sendgridKey.trim()) vars.push({ key: "SENDGRID_API_KEY", value: sendgridKey.trim() }); if (fromAddr.trim()) vars.push({ key: "EMAIL_FROM", value: fromAddr.trim() }); if (vars.length) save(vars); }; return ( } title="Email" subtitle="Needed before deploy for password resets, team invitations, share notifications, and dashboard email reports. Local development can run without it." connected={!envLoaded ? undefined : anyConfigured} open={open} onToggle={onToggle} > {!envLoaded ? ( ) : (
setEmailProvider(value as "resend" | "sendgrid") } /> {emailProvider === "resend" ? ( {resendConfigured ? (
RESEND_API_KEY configured
) : (
setResendKey(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveResend(); }} placeholder="re_..." className={emailInputCls} />
)} {fromConfigured ? (
EMAIL_FROM configured
) : (
setFromAddr(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveResend(); }} placeholder="From address - e.g. Acme " className={emailInputCls} /> {!resendConfigured ? null : ( )}
)}
) : ( {sendgridConfigured ? (
SENDGRID_API_KEY configured
) : (
setSendgridKey(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveSendgrid(); }} placeholder="SG...." className={emailInputCls} />
)} {fromConfigured ? (
EMAIL_FROM configured
) : (
setFromAddr(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveSendgrid(); }} placeholder="From address - e.g. Acme " className={emailInputCls} /> {!sendgridConfigured ? null : ( )}
)}
)}
)}
); } // ─── Agent Limits Section ────────────────────────────────────────────────── interface AgentLoopSettingsResponse { maxIterations: number; defaultMaxIterations: number; minMaxIterations: number; maxMaxIterations: number; scope: "org" | "user" | "default"; source: "org" | "user" | "env" | "default"; canUpdate: boolean; orgName?: string | null; role?: string | null; } function AgentLimitsSectionInner({ open, onToggle, }: { open?: boolean; onToggle?: () => void; }) { const isPage = useSettingsSurface() === "page"; const [settings, setSettings] = useState( null, ); const [value, setValue] = useState(""); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); const load = useCallback(() => { let cancelled = false; setLoading(true); fetch(agentNativePath("/_agent-native/agent-loop-settings")) .then((r) => (r.ok ? r.json() : null)) .then((data: AgentLoopSettingsResponse | null) => { if (cancelled || !data) return; setSettings(data); setValue(String(data.maxIterations)); }) .catch(() => {}) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); useEffect(() => load(), [load]); useEffect(() => { const handler = (event: Event) => { const detail = (event as CustomEvent).detail as | AgentLoopSettingsResponse | undefined; if (!detail?.maxIterations) return; setSettings(detail); setValue(String(detail.maxIterations)); }; window.addEventListener("agent-loop-settings:changed", handler); return () => window.removeEventListener("agent-loop-settings:changed", handler); }, []); const numericValue = Number(value); const hasPendingChange = !!settings && settings.canUpdate && Number.isInteger(numericValue) && numericValue !== settings.maxIterations; const scopeLabel = settings?.scope === "org" ? settings.orgName ? `${settings.orgName} organization` : "organization" : "your account"; const save = async () => { if (!settings?.canUpdate) return; setSaving(true); setSaved(false); setError(null); try { const res = await fetch( agentNativePath("/_agent-native/agent-loop-settings"), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ maxIterations: numericValue }), }, ); const body = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(body?.error ?? `Save failed (${res.status})`); } setSettings(body as AgentLoopSettingsResponse); setValue(String((body as AgentLoopSettingsResponse).maxIterations)); setSaved(true); window.dispatchEvent( new CustomEvent("agent-loop-settings:changed", { detail: body }), ); setTimeout(() => setSaved(false), 2000); } catch (err) { setError(err instanceof Error ? err.message : "Save failed"); } finally { setSaving(false); } }; const reset = async () => { if (!settings?.canUpdate) return; setSaving(true); setSaved(false); setError(null); try { const res = await fetch( agentNativePath("/_agent-native/agent-loop-settings"), { method: "DELETE" }, ); const body = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(body?.error ?? `Reset failed (${res.status})`); } setSettings(body as AgentLoopSettingsResponse); setValue(String((body as AgentLoopSettingsResponse).maxIterations)); window.dispatchEvent( new CustomEvent("agent-loop-settings:changed", { detail: body }), ); } catch (err) { setError(err instanceof Error ? err.message : "Reset failed"); } finally { setSaving(false); } }; return ( } title="Agent Limits" subtitle="Control how long a single agent response can work before pausing." connected={ loading ? undefined : settings ? settings.maxIterations !== settings.defaultMaxIterations : false } open={open} onToggle={onToggle} > {loading ? ( ) : settings ? (

Max iterations

Applies to {scopeLabel}. Default is{" "} {settings.defaultMaxIterations.toLocaleString()}.

{settings.source}
{ setValue(e.target.value); setError(null); }} onKeyDown={(e) => { if (e.key === "Enter" && hasPendingChange) void save(); }} className={cn( textInputClass(isPage), "min-w-0 flex-1 disabled:opacity-60", )} style={isPage ? CONTROL_STYLE_PAGE : undefined} />
{!settings.canUpdate && (

Only organization owners and admins can change this limit.

)} {error && (

{error}

)}
) : (

Agent limit settings are unavailable.

)}
); } // ─── Main SettingsPanel ───────────────────────────────────────────────────── export interface SettingsPanelProps { isDevMode: boolean; onToggleDevMode: () => void; showDevToggle: boolean; devAppUrl?: string; initialSection?: string | null; sectionRequestKey?: number; } // Agent capability modes. The internal values ("production"/"development") are // kept for back-compat with the AGENT_MODE wiring; only the visible labels // changed to "App mode" / "Code mode" so this control reads as the agent // capability it is — not the deployment environment (NODE_ENV). const agentModeOptions: SettingsSelectOption[] = [ { value: "production", label: "App mode", description: "App tools only; code, bash, and files require Builder or a local clone.", }, { value: "development", label: "Code mode", description: "Full access to code editing, bash, and files.", }, ]; function CapabilityStatusRow({ label, value, active, }: { label: string; value: React.ReactNode; active: boolean; }) { return (
{value}
); } function CapabilityStatusStrip({ isDevMode, builderConnected, builderLoading, builderBranchesAvailable, onOpenLlm, }: { isDevMode: boolean; builderConnected: boolean; builderLoading: boolean; builderBranchesAvailable: boolean; onOpenLlm: () => void; }) { const codeAvailable = isDevMode || (builderConnected && builderBranchesAvailable); const codeLabel = isDevMode ? "Local tools" : builderConnected && builderBranchesAvailable ? "Builder branches" : "Desktop/local"; return (
Available now
Connect ) } />
); } interface SettingsPanelContentProps extends SettingsPanelProps { sections?: readonly SettingsSectionId[]; showCapabilityStrip?: boolean; className?: string; surface?: SettingsSurface; builderConnectionOwnedExternally?: boolean; } function SettingsPanelContent({ isDevMode, onToggleDevMode, showDevToggle, devAppUrl, initialSection, sectionRequestKey, sections = ALL_SETTINGS_SECTIONS, showCapabilityStrip = true, className, surface = "sidebar", builderConnectionOwnedExternally = false, }: SettingsPanelContentProps) { const { status: builder, loading: builderLoading } = useBuilderStatus({ enabled: !builderConnectionOwnedExternally, }); const connected = builder?.configured ?? false; const connectUrl = builder?.cliAuthUrl ?? builder?.connectUrl; const orgName = builder?.orgName; const envManaged = !!builder?.envManaged; const credentialSource = builder?.credentialSource; const builderBranchesAvailable = !!builder?.builderEnabled; const builderFlow = useBuilderConnectFlow({ enabled: !builderConnectionOwnedExternally, popupUrl: connectUrl, trackingSource: "settings_panel_builder_card", }); const scrollSectionIntoView = useCallback((section: SettingsSectionId) => { window.requestAnimationFrame(() => { document.getElementById(settingsSectionDomId(section))?.scrollIntoView({ block: "start", behavior: "smooth", }); }); }, []); const { focusSecretKey, isSectionVisible: shouldShowSection, openSection, toggleSection: toggle, openSettingsSection: openControllerSection, } = useSettingsPanelController({ sections, initialSection, sectionRequestKey, onScrollToSection: scrollSectionIntoView, }); const openSettingsSection = useCallback( (section: SettingsSectionId, scroll = false) => openControllerSection(section, { scroll }), [openControllerSection], ); const isPage = surface === "page"; return (
{/* Agent capability mode (App vs Code) + app link */} {(showDevToggle || devAppUrl) && (
{showDevToggle && ( Open app in new tab ) : undefined } value={isDevMode ? "development" : "production"} options={agentModeOptions} onValueChange={(next) => { const nextIsDev = next === "development"; if (nextIsDev !== isDevMode) onToggleDevMode(); }} /> )}
)} {showCapabilityStrip && ( openSettingsSection("llm", true)} /> )} {/* LLM */} {shouldShowSection("llm") && ( toggle("llm")} /> )} {/* App default model */} {shouldShowSection("app-models") && ( toggle("app-models")} /> )} {/* Agent limits */} {shouldShowSection("limits") && ( toggle("limits")} /> )} {/* Voice transcription */} {shouldShowSection("voice") && ( } title="Voice Transcription" subtitle="How the composer microphone turns your voice into text." open={openSection === "voice"} onToggle={() => toggle("voice")} > )} {/* Demo mode */} {shouldShowSection("demo-mode") && ( } title="Demo mode" subtitle="Replace displayed emails with realistic fake data in this browser and reshape supported charts for presentations. Backend, MCP, and agent results stay real and access-scoped." open={openSection === "demo-mode"} onToggle={() => toggle("demo-mode")} > )} {/* Automations */} {shouldShowSection("automations") && ( } title="Automations" subtitle="Event-triggered and scheduled automations." open={openSection === "automations"} onToggle={() => toggle("automations")} > )} {/* API Keys & Connections */} {shouldShowSection("secrets") && ( } title="API Keys & Connections" subtitle="Service credentials and automation keys." open={openSection === "secrets"} onToggle={() => toggle("secrets")} > )} {/* Hosting */} {shouldShowSection("hosting") && ( } title="Hosting" subtitle="Deploy your app to the cloud." connected={connected} open={openSection === "hosting"} onToggle={() => toggle("hosting")} >
)} {/* Database */} {shouldShowSection("database") && ( } title="Database" subtitle="Connect a cloud database for persistent storage." connected={connected} open={openSection === "database"} onToggle={() => toggle("database")} >
)} {/* File uploads */} {shouldShowSection("uploads") && ( } title="File uploads" subtitle="Where user-uploaded files (avatars, chat attachments) are stored." connected={connected} open={openSection === "uploads"} onToggle={() => toggle("uploads")} >
)} {/* Authentication */} {shouldShowSection("auth") && ( } title="Authentication" subtitle="Set up user authentication and access control." connected={connected} open={openSection === "auth"} onToggle={() => toggle("auth")} >
)} {/* Email */} {shouldShowSection("email") && ( toggle("email")} /> )} {/* Browser Automation */} {shouldShowSection("browser") && ( } title="Browser Automation" subtitle="Let agents control a real browser for web tasks." connected={builderConnectionOwnedExternally ? undefined : connected} open={openSection === "browser"} onToggle={() => toggle("browser")} > {!builderConnectionOwnedExternally ? ( ) : null} )} {builderBranchesAvailable && shouldShowSection("background") && ( } title="Background Agent" subtitle="Make code changes from production mode via Builder." connected={connected} open={openSection === "background"} onToggle={() => toggle("background")} > )} {/* Integrations */} {shouldShowSection("integrations") && ( } title="Integrations" subtitle="Connect messaging platforms and external services." open={openSection === "integrations"} onToggle={() => toggle("integrations")} > )} {/* Usage & spend */} {shouldShowSection("usage") && ( } title="Usage" subtitle="Track token consumption and estimated cost — broken down by chat, automations, and background jobs." open={openSection === "usage"} onToggle={() => toggle("usage")} > )} {/* A2A Agents */} {shouldShowSection("a2a") && ( } title="Connected Agents (A2A)" subtitle="Manage remote agents connected via the A2A protocol." open={openSection === "a2a"} onToggle={() => toggle("a2a")} > )}
); } export function SettingsPanel(props: SettingsPanelProps) { return ; } function McpConnectionsCard() { const t = useT(); return (

{t("settings.mcpConnectionsTitle")}

{t("settings.mcpConnectionsDescription")}

{t("settings.openMcpConnections")}
); } export function ConnectionsSettingsContent({ settingsPanelProps, }: { settingsPanelProps: SettingsPanelProps; }) { return (
); } export function AgentSettingsContent({ className, }: { className?: string } = {}) { const { isDevMode, canToggle, setDevMode } = useDevMode(); const settingsPanelProps = useMemo( () => ({ isDevMode, onToggleDevMode: () => { void setDevMode(!isDevMode); }, showDevToggle: canToggle, }), [canToggle, isDevMode, setDevMode], ); return ( ); } export function useAgentSettingsTabs(): SettingsTabItem[] { const { isDevMode, canToggle, setDevMode } = useDevMode(); const baseProps = useMemo( () => ({ isDevMode, onToggleDevMode: () => { void setDevMode(!isDevMode); }, showDevToggle: canToggle, }), [canToggle, isDevMode, setDevMode], ); return useMemo(() => { const searchTabs = getAgentSettingsSearchTabs(); const searchTab = ( id: "agent" | "connections" | "organization" | "workspace", ) => { const tab = searchTabs.find((candidate) => candidate.id === id); if (!tab) throw new Error(`Missing agent workspace tab: ${id}`); return tab; }; const agent = searchTab("agent"); const connections = searchTab("connections"); const organization = searchTab("organization"); const workspace = searchTab("workspace"); return [ { ...connections, icon: IconPlugConnected, group: "workspace", content: , }, { ...organization, icon: IconUsersGroup, group: "workspace", content: (
), }, { ...workspace, icon: IconCloud, group: "workspace", content: ( ), }, { id: "extensions", label: "Extensions", icon: IconTool, group: "workspace", keywords: "extensions widgets mini apps tools sandboxed apps", content: , }, { ...agent, icon: IconHierarchy2, group: "manage-agent", href: "/agent#settings", searchEntries: undefined, content: , }, ]; }, [baseProps]); }