import { IconBook2, IconChecklist, IconClock, IconExternalLink, IconFolder, IconHistory, IconHelpCircle, IconHierarchy2, IconNotes, IconPlugConnected, IconTopologyRing2, IconSearch, IconSettings, IconShieldLock, IconX, } from "@tabler/icons-react"; import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode, } from "react"; import { MCP_CONNECT_GUIDES, MCP_CONNECT_MCP_URL_TEMPLATE, MCP_STATIC_TOKEN_FALLBACK, interpolateMcpConnectTemplate, type McpConnectTemplateValues, } from "../../shared/mcp-connect-content.js"; import { appPath } from "../api-path.js"; import { useT } from "../i18n.js"; import { useOrg } from "../org/hooks.js"; import { McpIntegrationDialog, // The dialog is intentionally reused here so the Agent page remains a thin // host for the existing MCP management flow. } from "../resources/McpIntegrationDialog.js"; import { McpServerDetail } from "../resources/McpServerDetail.js"; import type { ResourceView } from "../resources/ResourcesPanel.js"; import { useCreateMcpServer, useDeleteMcpServer, useMcpServers, type McpServer, type McpServerScope, } from "../resources/use-mcp-servers.js"; import { AGENT_SETTINGS_SECTIONS, buildSectionSearchEntries, } from "../settings/agent-settings-search.js"; import { AgentSettingsContent } from "../settings/SettingsPanel.js"; import type { SettingsSearchEntry, SettingsTabItem, } from "../settings/SettingsTabsPage.js"; import { cn } from "../utils.js"; import { AgentEmptyState } from "./AgentEmptyState.js"; import { AgentTabFrame } from "./AgentTabFrame.js"; import type { AgentPageScope, AgentPageTabProps } from "./types.js"; const AgentContextTab = lazy(() => import("./AgentContextTab.js").then((module) => ({ default: module.AgentContextTab, })), ); const AgentJobsTab = lazy(() => import("./AgentJobsTab.js").then((module) => ({ default: module.AgentJobsTab, })), ); const ResourcesPanel = lazy(() => import("../resources/ResourcesPanel.js").then((module) => ({ default: module.ResourcesPanel, })), ); type SettingsTabIcon = ComponentType<{ className?: string }>; function normalizeTabId(value?: string | null): string | null { const normalized = value ?.replace(/^#/, "") .trim() .toLowerCase() .replace(/["']/g, "") .replace(/[\s_]+/g, "-"); return normalized || null; } function resolveTabId( tabs: SettingsTabItem[], value?: string | null, ): string | null { const normalized = normalizeTabId(value); if (!normalized) return null; if (normalized === "context" && tabs.some((tab) => tab.id === "snapshots")) { return "snapshots"; } if (tabs.some((tab) => tab.id === normalized)) return normalized; const section = normalized.split(":", 1)[0]; const owner = tabs.find((tab) => tab.searchEntries?.some( (entry) => normalizeTabId(entry.hash ?? entry.id) === section, ), ); return owner?.id ?? null; } function updateTabHash(tabId: string) { if (typeof window === "undefined") return; const hash = tabId === "files" ? "" : `#${encodeURIComponent(tabId)}`; window.history.pushState( null, "", `${window.location.pathname}${window.location.search}${hash}`, ); } function TabLoading() { return (
); } function EmptySlot({ label }: { label: string }) { return (
{label}
); } export const AGENT_RESOURCE_DOCS_HREF: Record = { files: "https://agent-native.com/docs/agent-resources#resources-tab", instructions: "https://agent-native.com/docs/agent-resources#agents-md", agents: "https://agent-native.com/docs/agent-resources#custom-agents", memory: "https://agent-native.com/docs/agent-resources#memory", skills: "https://agent-native.com/docs/skills-guide", learnings: "https://agent-native.com/docs/agent-resources#memory", "remote-agents": "https://agent-native.com/docs/agent-resources#remote-vs-custom-agents", }; const RESOURCE_TAB_COPY: Record< ResourceView, { titleKey: string; descriptionKey: string } > = { files: { titleKey: "agentResources.files.title", descriptionKey: "agentResources.files.description", }, instructions: { titleKey: "agentResources.instructions.title", descriptionKey: "agentResources.instructions.description", }, agents: { titleKey: "agentResources.agents.title", descriptionKey: "agentResources.agents.description", }, memory: { titleKey: "agentResources.memory.title", descriptionKey: "agentResources.memory.description", }, skills: { titleKey: "agentResources.skills.title", descriptionKey: "agentResources.skills.description", }, learnings: { titleKey: "agentResources.learnings.title", descriptionKey: "agentResources.learnings.description", }, "remote-agents": { titleKey: "agentResources.remoteAgents.title", descriptionKey: "agentResources.remoteAgents.description", }, }; function AgentResourceTab({ scope, view, }: AgentPageTabProps & { view: ResourceView }) { const t = useT(); const copy = RESOURCE_TAB_COPY[view]; const title = t(copy.titleKey); return (
); } function ServerStatus({ server }: { server: McpServer }) { if (server.status.state === "connected") { return ( Connected · {server.status.toolCount} tools ); } if (server.status.state === "error") { return ( Connection error ); } return ( Status unknown ); } function ConnectionsTab({ canManageOrg = false }: AgentPageTabProps) { const t = useT(); const serversQuery = useMcpServers(); const createServer = useCreateMcpServer(); const deleteServer = useDeleteMcpServer(); const [dialogOpen, setDialogOpen] = useState(false); const [selectedServer, setSelectedServer] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [error, setError] = useState(null); const data = serversQuery.data; const hasOrg = Boolean(data?.orgId); const canCreateOrgMcp = hasOrg && canManageOrg; const onCreateMcpServer = useCallback( async (args: { scope: McpServerScope; name: string; url: string; headers?: Record; description?: string; }) => { if (args.scope === "org" && !canCreateOrgMcp) { throw new Error( "Only organization admins can add organization MCP servers.", ); } return createServer.mutateAsync(args); }, [canCreateOrgMcp, createServer], ); const removeServer = async (server: McpServer) => { const key = `${server.scope}:${server.id}`; if (deleteTarget !== key) { setDeleteTarget(key); return; } setError(null); try { await deleteServer.mutateAsync({ id: server.id, scope: server.scope }); if ( selectedServer?.id === server.id && selectedServer.scope === server.scope ) { setSelectedServer(null); } setDeleteTarget(null); } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); } }; const renderServer = (server: McpServer) => { const key = `${server.scope}:${server.id}`; const canDelete = server.scope === "user" || canManageOrg; const selected = selectedServer?.id === server.id && selectedServer.scope === server.scope; return (
{canDelete && ( )}
{selected && (
)}
); }; return ( { setError(null); setDialogOpen(true); }} className="cursor-pointer rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90" > {t("mcpIntegrations.connect")} } >
{error && (

{error}

)} {serversQuery.isLoading ? ( ) : serversQuery.isError ? (

Could not load MCP servers.

) : (
{[ { label: "Personal", servers: data?.user ?? [] }, { label: "Organization", servers: data?.org ?? [] }, ].map((section) => (

{section.label}

{section.servers.length > 0 ? (
{section.servers.map(renderServer)}
) : ( )}
))}
)}
); } interface AccessUrls { appName: string; appUrl: string; mcpUrl: string; connectUrl: string; agentCardUrl: string; } export const AGENT_ACCESS_DOCS_HREF = { mcp: "https://agent-native.com/docs/mcp-protocol", a2a: "https://agent-native.com/docs/a2a-protocol", } as const; interface CopyFieldProps { label: string; value: string; docsHref?: string; docsLabel?: string; } function CopyField({ label, value, docsHref, docsLabel }: CopyFieldProps) { const [copied, setCopied] = useState(false); const copy = async () => { try { await navigator.clipboard.writeText(value); setCopied(true); window.setTimeout(() => setCopied(false), 1600); } catch { setCopied(false); } }; return (
{label} {docsHref && ( )}
{value}
); } function AccessTab({ appName: appNameProp, }: AgentPageTabProps & { appName?: string }) { const [urls, setUrls] = useState(null); const [agentCardAvailable, setAgentCardAvailable] = useState(false); const [activeGuide, setActiveGuide] = useState(MCP_CONNECT_GUIDES[0]?.id); useEffect(() => { const origin = window.location.origin; const baseUrl = new URL(appPath("/"), origin).toString().replace(/\/$/, ""); const hostname = window.location.hostname || "app"; const metaSiteName = document .querySelector('meta[property="og:site_name"]') ?.getAttribute("content") ?.trim(); const hostnameGuess = hostname !== "localhost" && hostname !== "127.0.0.1" ? hostname.split(".")[0] : ""; const appName = appNameProp?.trim() || metaSiteName || hostnameGuess || "this app"; const templateValues = { appName, appUrl: baseUrl, mcpUrl: "", serverId: `agent-native-${hostname}`, } satisfies McpConnectTemplateValues; setUrls({ appName, appUrl: baseUrl, mcpUrl: interpolateMcpConnectTemplate( MCP_CONNECT_MCP_URL_TEMPLATE, templateValues, ), connectUrl: new URL(appPath("/mcp/connect"), origin).toString(), agentCardUrl: new URL( appPath("/.well-known/agent-card.json"), origin, ).toString(), }); }, [appNameProp]); useEffect(() => { if (!urls) return; let cancelled = false; fetch(urls.agentCardUrl) .then((response) => { if (!cancelled) setAgentCardAvailable(response.ok); }) .catch(() => { if (!cancelled) setAgentCardAvailable(false); }); return () => { cancelled = true; }; }, [urls]); const templateValues: McpConnectTemplateValues | null = urls ? { appName: urls.appName, appUrl: urls.appUrl, mcpUrl: urls.mcpUrl, serverId: `agent-native-${window.location.hostname || "app"}`, } : null; const guide = MCP_CONNECT_GUIDES.find((item) => item.id === activeGuide) ?? MCP_CONNECT_GUIDES[0]; return (
{urls ? ( <> {agentCardAvailable && ( )}

Client setup

These instructions are also available on the full connect page.

{MCP_CONNECT_GUIDES.map((item) => ( ))}
{guide && templateValues && (
{guide.steps?.length ? (
    {guide.steps.map((step) => (
  1. {interpolateMcpConnectTemplate(step, templateValues)}
  2. ))}
) : null} {guide.intro && (

{interpolateMcpConnectTemplate( guide.intro, templateValues, )}

)} {guide.commandTemplate && ( )} {guide.configTemplate && ( )} {guide.action?.kind === "link" && guide.action.href && ( {guide.action.label} )} {guide.note && (

{interpolateMcpConnectTemplate( guide.note, templateValues, )}

)}
)}

{MCP_STATIC_TOKEN_FALLBACK.title}

{MCP_STATIC_TOKEN_FALLBACK.state}. Open the connect page to create a token for clients that cannot complete OAuth.

Open full connect page
) : ( )}
); } export interface AgentPageExtraTabContext extends AgentPageTabProps { scopeControl: ReactNode; } export type AgentPageExtraTabFactory = ( context: AgentPageExtraTabContext, ) => SettingsTabItem; export interface AgentTabsPageProps { /** * Human-readable app name used in the Access tab's connect instructions * (e.g. "name it Mail"). Falls back to the `og:site_name` meta tag, then a * hostname-derived guess — never `document.title`, which this page owns. */ appName?: string; extraTabs?: SettingsTabItem[]; /** Scoped app-specific tabs that receive the current Manage agent page scope. */ extraTabFactories?: AgentPageExtraTabFactory[]; defaultTab?: string; className?: string; /** Whether to render the Agent page search box. Defaults to true. */ enableSearch?: boolean; searchPlaceholder?: string; hiddenTabs?: string[]; value?: string; onValueChange?: (tabId: string) => void; } export function AgentTabsPage({ appName, extraTabs = [], extraTabFactories = [], defaultTab = "files", className, enableSearch = true, searchPlaceholder = "Search", hiddenTabs = [], value, onValueChange, }: AgentTabsPageProps) { const t = useT(); const { data: org } = useOrg(); const canManageOrg = !org?.orgId || org.role === "owner" || org.role === "admin"; const scope: AgentPageScope = "user"; const rootRef = useRef(null); const searchRef = useRef(null); const [query, setQuery] = useState(""); const normalizedHiddenTabs = useMemo( () => new Set(hiddenTabs.map((tab) => normalizeTabId(tab)).filter(Boolean)), [hiddenTabs], ); const scopeControl: ReactNode = null; const scopedExtraTabs = useMemo( () => extraTabFactories.map((factory) => factory({ scope, canManageOrg, scopeControl }), ), [canManageOrg, extraTabFactories, scope, scopeControl], ); const tabs = useMemo( () => [ { id: "files", label: "Files", icon: IconFolder, group: "resources", keywords: "workspace plain files documents uploads", content: ( }> ), }, { id: "instructions", label: "Instructions", icon: IconChecklist, group: "resources", keywords: "agents md rules preferences guidance", content: ( }> ), }, { id: "agents", label: "Agents", icon: IconHierarchy2, group: "resources", keywords: "custom sub agents delegate profiles", content: ( }> ), }, { id: "memory", label: "Memory", icon: IconNotes, group: "resources", keywords: "long term notes memory index recall", content: ( }> ), }, { id: "skills", label: "Skills", icon: IconBook2, group: "resources", keywords: "abilities reusable instructions workflows", content: ( }> ), }, { id: "learnings", label: "Learnings", icon: IconHistory, group: "resources", keywords: "corrections patterns knowledge feedback", content: ( }> ), }, { id: "remote-agents", label: "Remote agents", icon: IconTopologyRing2, group: "resources", keywords: "a2a connected remote agents delegation", content: ( }> ), }, { id: "snapshots", label: "Snapshots", icon: IconHistory, group: "agent", keywords: "context recent loads provenance tokens", content: ( }> ), }, { id: "connections", label: "Connections", icon: IconPlugConnected, group: "agent", keywords: "mcp servers tools integrations", searchEntries: [ { id: "mcp-servers", label: "MCP servers", keywords: "tools" }, ], content: , }, { id: "jobs", label: "Jobs", icon: IconClock, group: "agent", keywords: "scheduled automations recurring", content: ( }> ), }, { id: "settings", label: t("settings.agentTitle"), icon: IconSettings, group: "agent", keywords: "agent settings model llm api keys limits voice automations", searchEntries: buildSectionSearchEntries(AGENT_SETTINGS_SECTIONS), content: ( ), }, { id: "access", label: "Access", icon: IconShieldLock, group: "agent", keywords: "external clients oauth a2a exposure", searchEntries: [ { id: "mcp-connect", label: "External client setup", keywords: "oauth connect", }, { id: "a2a-agent-card", label: "A2A agent card", keywords: "agent card", }, ], content: ( ), }, ...extraTabs, ...scopedExtraTabs, ], [appName, canManageOrg, extraTabs, scope, scopedExtraTabs], ); const visibleTabs = useMemo( () => tabs.filter((tab) => !normalizedHiddenTabs.has(tab.id)), [normalizedHiddenTabs, tabs], ); const fallbackTab = visibleTabs.some((tab) => tab.id === defaultTab) ? defaultTab : (visibleTabs[0]?.id ?? "context"); const [internalTab, setInternalTab] = useState(() => { if (typeof window === "undefined") return fallbackTab; return resolveTabId(visibleTabs, window.location.hash) ?? fallbackTab; }); const isControlled = value !== undefined; const activeTab = isControlled ? value : internalTab; const selectedTab = visibleTabs.find((tab) => tab.id === activeTab) ?? visibleTabs[0]; const tabGroups = useMemo(() => { const groups: Array<{ id: string; tabs: SettingsTabItem[] }> = []; for (const tab of visibleTabs) { const groupId = tab.group ?? "agent"; const previous = groups.at(-1); if (previous?.id === groupId) previous.tabs.push(tab); else groups.push({ id: groupId, tabs: [tab] }); } return groups; }, [visibleTabs]); useEffect(() => { if (!isControlled && !visibleTabs.some((tab) => tab.id === internalTab)) { setInternalTab(fallbackTab); } }, [fallbackTab, internalTab, isControlled, visibleTabs]); useEffect(() => { if (isControlled) return; const onHashChange = () => { const next = resolveTabId(visibleTabs, window.location.hash); if (next) setInternalTab(next); }; window.addEventListener("hashchange", onHashChange); return () => window.removeEventListener("hashchange", onHashChange); }, [isControlled, visibleTabs]); const changeTab = useCallback( (tabId: string) => { if (!isControlled) setInternalTab(tabId); onValueChange?.(tabId); }, [isControlled, onValueChange], ); const searchIndex = useMemo(() => { const entries: Array< SettingsSearchEntry & { tabId: string; haystack: string } > = []; for (const tab of visibleTabs) { entries.push({ id: `tab:${tab.id}`, label: tab.label, keywords: tab.keywords, tabId: tab.id, haystack: `${tab.label} ${tab.keywords ?? ""}`.toLowerCase(), }); for (const entry of tab.searchEntries ?? []) { entries.push({ ...entry, tabId: entry.tabId ?? tab.id, haystack: `${entry.label} ${entry.keywords ?? ""} ${entry.description ?? ""} ${tab.label}`.toLowerCase(), }); } } return entries; }, [visibleTabs]); const results = useMemo(() => { const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean); if (!terms.length) return []; return searchIndex .filter((entry) => terms.every((term) => entry.haystack.includes(term))) .sort((a, b) => a.label.localeCompare(b.label)); }, [query, searchIndex]); const selectSearchResult = useCallback( (entry: (typeof searchIndex)[number]) => { changeTab(entry.tabId); setQuery(""); if (isControlled || typeof window === "undefined") return; if (entry.hash) { window.history.pushState( null, "", `${window.location.pathname}${window.location.search}#${entry.hash.replace(/^#/, "")}`, ); window.dispatchEvent(new Event("hashchange")); } else { updateTabHash(entry.tabId); } }, [changeTab, isControlled], ); return (
{enableSearch ? (
setQuery(event.target.value)} onKeyDown={(event) => { if (event.key === "Escape") setQuery(""); if (event.key === "Enter" && results[0]) { event.preventDefault(); selectSearchResult(results[0]); } }} placeholder={searchPlaceholder} aria-label={searchPlaceholder} className="h-8 w-full rounded-md border border-border bg-background ps-8 pe-7 text-[13px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-foreground/30 focus:ring-2 focus:ring-accent/40" /> {query && ( )}
) : null} {query.trim() ? (
{results.length === 0 ? (

No matching items

) : ( results.map((entry) => ( )) )}
) : ( )}
{selectedTab?.content ?? }
); }