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) => (
-
{interpolateMcpConnectTemplate(step, templateValues)}
))}
) : 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 ?? }
);
}