import { forwardRef, useId, useMemo, useState, type ComponentPropsWithoutRef, type Dispatch, type ReactNode, type SetStateAction, } from "react"; import { Check, ChevronDown, ChevronRight, Clipboard, Database, ExternalLink, Loader2, PauseCircle, PlayCircle, Plus, RotateCcw, Search, Server, SlidersHorizontal, TriangleAlert, Trash2, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { DismissibleStatusMessage, RestartRequiredNotice, SETTINGS_SEARCH_INPUT_CLASS, SettingsSectionTitle, } from "@/components/settings/shared/SettingsControls"; import { McpManagementDialog, type McpManagementTab, } from "@/components/settings/system/McpManagementDialog"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { SegmentedControl } from "@/components/ui/segmented-control"; import { Textarea } from "@/components/ui/textarea"; import { useLogoFallback } from "@/hooks/useLogoFallback"; import { isGenericRepositoryLogoUrl, logoFallbackUrls } from "@/lib/provider-brand"; import type { CliAppInfo, CliAppsPayload, McpOAuthFlowPayload, McpPresetInfo, McpPresetsPayload, } from "@/lib/types"; import { cn } from "@/lib/utils"; export type AppsKindFilter = "ready" | "cli" | "mcp"; type AppsCatalogItem = | { id: string; kind: "cli"; app: CliAppInfo } | { id: string; kind: "mcp"; preset: McpPresetInfo }; type CustomMcpTransport = "stdio" | "streamableHttp" | "sse"; type CustomMcpAuth = "none" | "oauth" | "headers"; export const CLI_APPS_REFRESH_RETRY_MS = 2_000; export const CLI_APPS_REFRESH_MAX_RETRIES = 30; export interface CustomMcpForm { name: string; transport: CustomMcpTransport; auth: CustomMcpAuth; command: string; args: string; url: string; env: string; headers: string; toolTimeout: string; } export const DEFAULT_CUSTOM_MCP_FORM: CustomMcpForm = { name: "", transport: "stdio", auth: "none", command: "", args: "", url: "", env: "", headers: "", toolTimeout: "30", }; export function AppsCatalogSettings({ cliApps, mcpPresets, cliAppsLoading, mcpPresetsLoading, query, filter, cliActionKey, mcpActionKey, mcpOAuthFlow, mcpOAuthPopupBlocked, mcpOAuthCallbackUrl, mcpOAuthCompleting, mcpOAuthCallbackError, cliMessage, cliError, cliFocusName, mcpMessage, mcpError, mcpFieldValues, customMcpForm, mcpConfigImport, showBrandLogos, requiresRestartPending, onQueryChange, onFilterChange, onCliAction, onMcpAction, onMcpOAuthConnect, onMcpOAuthCancel, onMcpOAuthOpen, onMcpOAuthCallbackUrlChange, onMcpOAuthComplete, onDismissStatus, onBackToChat, onMcpFieldChange, onCustomMcpFormChange, onMcpConfigImportChange, onSaveCustomMcp, onImportMcpConfig, onMcpToolsChange, onRestart, isRestarting, }: { cliApps: CliAppsPayload | null; mcpPresets: McpPresetsPayload | null; cliAppsLoading: boolean; mcpPresetsLoading: boolean; query: string; filter: AppsKindFilter; cliActionKey: string | null; mcpActionKey: string | null; mcpOAuthFlow: McpOAuthFlowPayload | null; mcpOAuthPopupBlocked: boolean; mcpOAuthCallbackUrl: string; mcpOAuthCompleting: boolean; mcpOAuthCallbackError: string | null; cliMessage: string | null; cliError: string | null; cliFocusName: string | null; mcpMessage: string | null; mcpError: string | null; mcpFieldValues: Record>; customMcpForm: CustomMcpForm; mcpConfigImport: string; showBrandLogos: boolean; requiresRestartPending: boolean; onQueryChange: (value: string) => void; onFilterChange: (value: AppsKindFilter) => void; onCliAction: (action: "install" | "update" | "uninstall" | "test", name: string) => void; onMcpAction: (action: "enable" | "disable" | "remove" | "test" | "reconnect", name: string, values?: Record) => void; onMcpOAuthConnect: (name: string, reset?: boolean) => void; onMcpOAuthCancel: () => void; onMcpOAuthOpen: () => void; onMcpOAuthCallbackUrlChange: (value: string) => void; onMcpOAuthComplete: () => void; onDismissStatus: () => void; onBackToChat: () => void; onMcpFieldChange: (presetName: string, fieldName: string, value: string) => void; onCustomMcpFormChange: Dispatch>; onMcpConfigImportChange: (value: string) => void; onSaveCustomMcp: () => void; onImportMcpConfig: () => void; onMcpToolsChange: (name: string, enabledTools: string[]) => void; onRestart?: () => void; isRestarting?: boolean; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const filterOptions = [ { value: "ready", label: tx("settings.apps.filterAll", "Ready") }, { value: "cli", label: tx("settings.apps.filterCli", "Apps") }, { value: "mcp", label: tx("settings.apps.filterMcp", "MCP") }, ]; const normalizedQuery = query.trim().toLowerCase(); const items: AppsCatalogItem[] = [ ...(cliApps?.apps ?? []).map((app) => ({ id: `cli:${app.name}`, kind: "cli" as const, app })), ...(mcpPresets?.presets ?? []).map((preset) => ({ id: `mcp:${preset.name}`, kind: "mcp" as const, preset, })), ] .filter((item) => { if (normalizedQuery) return appsSearchText(item).includes(normalizedQuery); if (filter === "ready") return appsReady(item); if (filter === "cli") { return item.kind === "cli" || item.preset.source === "agent-plugin"; } return item.kind === "mcp" && item.preset.source !== "agent-plugin"; }) .sort((left, right) => { const rank = Number(!appsReady(left)) - Number(!appsReady(right)); return rank || appsTitle(left).localeCompare(appsTitle(right)); }); const focusedApp = cliFocusName ? (cliApps?.apps ?? []).find((app) => app.name === cliFocusName && app.installed) : null; const loading = (cliAppsLoading || mcpPresetsLoading) && !cliApps && !mcpPresets; const cliAppCount = cliApps?.apps.length ?? 0; const emptyTitle = normalizedQuery ? tx("settings.apps.empty", "No tools match your search.") : filter === "cli" ? tx("settings.apps.emptyApps", "No apps available.") : filter === "mcp" ? tx("settings.apps.emptyIntegrations", "No MCP tools available.") : tx("settings.apps.emptyReady", "No tools are ready yet."); const emptyBrowseTarget: AppsKindFilter | null = normalizedQuery ? null : filter === "cli" ? "mcp" : filter === "mcp" ? (cliAppCount ? "cli" : null) : cliAppCount ? "cli" : "mcp"; const statusMessage = cliError || mcpError || (!focusedApp ? cliMessage || mcpMessage : null); const statusIsError = Boolean(cliError || mcpError); const oauthStatusAnnouncement = mcpOAuthFlow ? mcpOAuthStatusText( mcpOAuthFlow.status, mcpOAuthPopupBlocked, tx, mcpOAuthFlow.completion_input, ) : ""; return (
{oauthStatusAnnouncement}
onQueryChange(event.target.value)} placeholder={tx("settings.apps.searchPlaceholder", "Search Apps")} className={cn( "h-12 rounded-[14px] pl-11 text-[15px]", SETTINGS_SEARCH_INPUT_CLASS, )} />
onFilterChange(value as AppsKindFilter)} />
{statusMessage ? ( ) : null} {focusedApp ? ( ) : null} {requiresRestartPending ? ( ) : null}
{filter === "mcp" ? tx("settings.apps.mcpTools", "MCP tools") : tx("settings.apps.featured", "Tools")} {items.length}
{loading ? (
{tx("settings.apps.loading", "Loading Apps...")}
) : items.length ? (
{items.map((item) => item.kind === "cli" ? ( ) : ( ), )}
) : (

{emptyTitle}

{normalizedQuery ? ( ) : emptyBrowseTarget ? ( ) : (

{tx( "settings.apps.emptyIntegrationsHint", "Add a custom MCP server below.", )}

)}
)}
{filter === "mcp" ? ( ) : null}
); } function CliAppsCatalogRow({ app, actionKey, showBrandLogos, onAction, }: { app: CliAppInfo; actionKey: string | null; showBrandLogos: boolean; onAction: (action: "install" | "update" | "uninstall" | "test", name: string) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const installBusy = actionKey === `install:${app.name}`; const updateBusy = actionKey === `update:${app.name}`; const uninstallBusy = actionKey === `uninstall:${app.name}`; const testBusy = actionKey === `test:${app.name}`; const busy = installBusy || updateBusy || uninstallBusy || testBusy; const description = app.description || app.requires || app.entry_point || app.name; return (

{app.display_name}

{tx("settings.apps.cliLabel", "App")}

{description}

{app.installed ? ( <> onAction("test", app.name)}> {tx("settings.cliApps.test", "Test CLI")} onAction("update", app.name)}> {tx("settings.cliApps.update", "Update CLI")} onAction("uninstall", app.name)} > {tx("settings.cliApps.uninstall", "Uninstall CLI")} onAction("uninstall", app.name)} > ) : app.install_supported ? ( onAction("install", app.name)} > ) : ( )}
); } function McpAppsCatalogRow({ preset, values, actionKey, oauthFlow, oauthPopupBlocked, oauthCallbackUrl, oauthCompleting, oauthCallbackError, showBrandLogos, showTypeBadge, onFieldChange, onAction, onOAuthConnect, onOAuthCancel, onOAuthOpen, onOAuthCallbackUrlChange, onOAuthComplete, onToolsChange, }: { preset: McpPresetInfo; values: Record; actionKey: string | null; oauthFlow: McpOAuthFlowPayload | null; oauthPopupBlocked: boolean; oauthCallbackUrl: string; oauthCompleting: boolean; oauthCallbackError: string | null; showBrandLogos: boolean; showTypeBadge: boolean; onFieldChange: (presetName: string, fieldName: string, value: string) => void; onAction: (action: "enable" | "disable" | "remove" | "test" | "reconnect", name: string, values?: Record) => void; onOAuthConnect: (name: string, reset?: boolean) => void; onOAuthCancel: () => void; onOAuthOpen: () => void; onOAuthCallbackUrlChange: (value: string) => void; onOAuthComplete: () => void; onToolsChange: (name: string, enabledTools: string[]) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const [managementOpen, setManagementOpen] = useState(false); const [managementTab, setManagementTab] = useState("overview"); const enableBusy = actionKey === `enable:${preset.name}`; const disableBusy = actionKey === `disable:${preset.name}`; const removeBusy = actionKey === `remove:${preset.name}`; const testBusy = actionKey === `test:${preset.name}`; const reconnectBusy = actionKey === `reconnect:${preset.name}`; const toolsBusy = actionKey === `tools:${preset.name}`; const oauthBusy = actionKey === `oauth:${preset.name}`; const anotherOAuthBusy = Boolean(actionKey?.startsWith("oauth:")) && !oauthBusy; const busy = enableBusy || disableBusy || removeBusy || testBusy || reconnectBusy || toolsBusy || oauthBusy; const agentPlugin = preset.source === "agent-plugin"; const toggleable = preset.enabled !== undefined; const isOAuth = preset.auth === "oauth"; const missingFields = preset.required_fields.filter((field) => field.required && !field.configured); const hasFields = preset.required_fields.length > 0; const needsSetupInput = missingFields.length > 0; const configuredInstalled = preset.installed && preset.configured; const readyInstalled = preset.enabled ?? configuredInstalled; const runtimeConnected = !toggleable && preset.runtime_status === "connected"; const runtimeConnecting = !toggleable && preset.runtime_status === "connecting"; const runtimeFailed = !toggleable && preset.runtime_status === "failed"; const statusLabel = toggleable ? tx("settings.nanobotFeatures.enabled", "Enabled") : runtimeConnected ? tx("connection.open", "Connected") : mcpPresetStatusLabel(preset.status, tx); const failureLabel = tx("settings.mcp.connectionFailed", "Connection failed."); const failureStatusLabel = failureLabel.replace(/[.!。!]+$/u, ""); const description = tx( `settings.mcp.presetDescriptions.${preset.name}`, preset.description || preset.note || preset.name, ); const detail = agentPlugin && preset.requires ? `${description} · ${preset.requires}` : description || preset.requires; const manualCallback = oauthFlow?.completion_input === "callback_url" && Boolean(oauthFlow.authorization_url); const callbackInputId = `mcp-oauth-callback-${preset.name}`; const callbackHelpId = `${callbackInputId}-help`; const callbackErrorId = `${callbackInputId}-error`; const enableOrOpenSetup = () => { if (isOAuth) { onOAuthConnect(preset.name); return; } if (needsSetupInput || (preset.installed && !preset.configured && hasFields)) { setManagementTab("connection"); setManagementOpen(true); return; } onAction("enable", preset.name, values); }; const openManagement = (tab: McpManagementTab = "overview") => { setManagementTab(tab); setManagementOpen(true); }; return (

{preset.display_name}

{showTypeBadge ? ( {agentPlugin ? tx("settings.apps.filterPlugins", "Plugins") : tx("settings.apps.mcpLabel", "MCP")} ) : null}

{runtimeFailed && configuredInstalled ? ( ) : null} {runtimeFailed && configuredInstalled ? failureLabel : detail}

{oauthFlow ? ( <> ) : runtimeConnecting && configuredInstalled ? ( <> onAction("remove", preset.name)} > ) : runtimeFailed && configuredInstalled ? ( openManagement("connection")} > ) : readyInstalled ? ( toggleable ? ( onAction("disable", preset.name)} > {tx("settings.nanobotFeatures.disable", "Disable")} ) : ( openManagement("overview")} > ) ) : preset.enabled === false ? ( onAction("enable", preset.name, values)} /> ) : isOAuth && preset.install_supported ? ( onOAuthConnect(preset.name)} /> ) : preset.installed && !preset.configured ? ( { if (hasFields) openManagement("connection"); else onAction("enable", preset.name, values); }} /> ) : preset.install_supported ? ( ) : ( )}
{manualCallback ? (
{ event.preventDefault(); onOAuthComplete(); }} >

{t("settings.oauth.pasteCallbackToContinue")}

{tx( "settings.mcp.manualCallbackHelp", "After approving access, the localhost page will not load. Copy its full URL from the address bar and paste it here.", )}