"use client" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { useCustomer } from "autumn-js/react" import { cn } from "@lib/utils" import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" import { SectionRail } from "@/components/directory/section-rail" import { $fetch } from "@lib/api" import { authClient } from "@lib/auth" import { useAuth } from "@lib/auth-context" import type { ConnectionResponseSchema, DocumentsWithMemoriesResponseSchema, } from "@repo/validation/api" import type { z } from "zod" import { Button } from "@ui/components/button" import { ChromeIcon, AppleShortcutsIcon, RaycastIcon, } from "@/components/integration-icons" import { GoogleDrive, Notion, OneDrive, MCPIcon, Granola, } from "@ui/assets/icons" import * as DialogPrimitive from "@radix-ui/react-dialog" import { ArrowLeft, ArrowRight, BookOpen, Check, ChevronDown, ExternalLink, FileText, Globe, Info, Loader, Plus, Search, X, Zap, } from "lucide-react" import { formatRelativeTime } from "@/components/settings/sync-utils" import { useConnectorAccess } from "@/hooks/use-connector-access" import { useConnectionHealth } from "@/hooks/use-connection-health" import { useContainerTags } from "@/hooks/use-container-tags" import { DEFAULT_PROJECT_ID } from "@lib/constants" import { CHROME_EXTENSION_URL, POKE_RECIPE_URL } from "@lib/constants" import { analytics } from "@/lib/analytics" import Image from "next/image" import { useViewMode } from "@/lib/view-mode-context" import type { ViewParamValue } from "@/lib/search-params" import { parseAsString, parseAsStringEnum, useQueryState } from "nuqs" import { addDocumentParam, docParam } from "@/lib/search-params" import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode, } from "react" import { AnimatePresence, motion } from "motion/react" import { toast } from "sonner" import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" import { PLUGIN_CATALOG, FREE_TIER_PLUGIN_IDS, isFreeTierPlugin, normalizePluginClientId, type InstallStep, type PluginInfo, } from "@/lib/plugin-catalog" import { CopyButton, INSET, InstallSteps, PillButton, } from "./integrations/install-steps" import { ShortcutsConnectButtons, useShortcutsConnect, } from "./integrations/shortcuts-detail" import { MCPSteps } from "./mcp-modal/mcp-detail-view" import { GranolaConnectModal } from "./granola-connect-modal" import { detectPluginSpace, detectPluginSource } from "@/lib/plugin-space" import { usePromoCode } from "@/hooks/use-promo-code" type Connection = z.infer type ConnectorProvider = "google-drive" | "notion" | "onedrive" | "granola" interface ConnectedKey { keyId: string keyStart: string | null pluginId: string lastRequest?: string | null createdAt?: string | null } interface ConnectedMcpKey { keyId: string keyStart: string | null lastRequest?: string | null createdAt?: string | null } function isMcpAuthMetadata(metadata: { sm_source?: string; sm_kind?: string }) { return ( metadata.sm_source === "mcp" || metadata.sm_kind === "mcp_oauth_exchange" ) } function toIsoDate(value: string | Date | null | undefined): string | null { if (!value) return null const d = value instanceof Date ? value : new Date(value) if (Number.isNaN(d.getTime())) return null return d.toISOString() } function toMs(value: string | null | undefined): number { if (!value) return 0 const t = new Date(value).getTime() return Number.isNaN(t) ? 0 : t } function compactRelativeTime(value: number | string): string { return formatRelativeTime(value).replace(/\s*ago$/i, "") } function parsePluginAuthKeys( apiKeys: ListedApiKey[], keyPrefix: (key: ListedApiKey) => string | null, ): { active: ConnectedKey[]; setup: ConnectedKey[] } { const active: ConnectedKey[] = [] const setup: ConnectedKey[] = [] for (const key of apiKeys) { if (key.enabled === false) continue if (!key.metadata) continue try { const metadata = typeof key.metadata === "string" ? (JSON.parse(key.metadata) as { sm_type?: string sm_client?: string }) : (key.metadata as { sm_type?: string; sm_client?: string }) if (metadata.sm_type !== "plugin_auth" || !metadata.sm_client) continue const entry: ConnectedKey = { keyId: key.id, keyStart: keyPrefix(key), pluginId: normalizePluginClientId(metadata.sm_client), lastRequest: toIsoDate(key.lastRequest), createdAt: toIsoDate(key.createdAt), } if (key.lastRequest) active.push(entry) else setup.push(entry) } catch {} } return { active, setup } } function parseMcpAuthKeys( apiKeys: ListedApiKey[], keyPrefix: (key: ListedApiKey) => string | null, ): ConnectedMcpKey[] { const keys: ConnectedMcpKey[] = [] for (const key of apiKeys) { if (key.enabled === false) continue if (!key.metadata) continue try { const metadata = typeof key.metadata === "string" ? (JSON.parse(key.metadata) as { sm_source?: string sm_kind?: string }) : (key.metadata as { sm_source?: string; sm_kind?: string }) if (!isMcpAuthMetadata(metadata)) continue keys.push({ keyId: key.id, keyStart: keyPrefix(key), lastRequest: toIsoDate(key.lastRequest), createdAt: toIsoDate(key.createdAt), }) } catch {} } return keys } type ListedApiKey = { id: string name?: string | null createdAt?: string | Date | null enabled?: boolean lastRequest?: string | Date | null metadata: string | Record | null start?: string | null } type ItemKind = "plugin" | "connector" | "client" | "mcp-client" | "import" type MCPClientKey = | "antigravity" | "chatgpt" | "codex" | "cursor" | "claude" | "vscode" | "cline" | "gemini-cli" | "claude-code" | "mcp-url" const MCP_CLIENTS: Array<{ key: MCPClientKey name: string tagline: string simpleTitle?: string dev?: boolean }> = [ { key: "antigravity", name: "Antigravity", tagline: "MCP config for Antigravity", simpleTitle: "Bring your memory into Antigravity", dev: true, }, { key: "cursor", name: "Cursor", tagline: "One-click MCP install in Cursor", simpleTitle: "Code with your saved knowledge nearby", }, { key: "claude", name: "Claude Desktop", tagline: "Connect supermemory in Claude Desktop", simpleTitle: "Reference your notes during any Claude chat", }, { key: "chatgpt", name: "ChatGPT", tagline: "Apps via ChatGPT developer mode", simpleTitle: "Let ChatGPT recall what you've saved", }, { key: "vscode", name: "VS Code", tagline: "Native MCP support in VS Code", simpleTitle: "Pull your knowledge into VS Code while coding", dev: true, }, { key: "cline", name: "Cline", tagline: "MCP via the Cline VS Code extension", simpleTitle: "Cline can read and add to your memory", dev: true, }, { key: "gemini-cli", name: "Gemini CLI", tagline: "Google Gemini terminal client", simpleTitle: "Bring your memory into Gemini sessions", dev: true, }, { key: "codex", name: "Codex (MCP)", tagline: "OpenAI Codex CLI via MCP config", simpleTitle: "Codex with access to your saved context", dev: true, }, { key: "claude-code", name: "Claude Code (MCP)", tagline: "Connect via Claude Code MCP config", simpleTitle: "Claude Code with your project context", dev: true, }, { key: "mcp-url", name: "MCP URL", tagline: "Use the URL in any custom MCP client", simpleTitle: "Connect any MCP-capable app to supermemory", dev: true, }, ] function mcpClientIconSrc(key: MCPClientKey): string { if (key === "mcp-url") return "/mcp-icon.svg" if (key === "antigravity") return "/mcp-supported-tools/antigravity.png" const file = key === "claude-code" ? "claude" : key return `/mcp-supported-tools/${file}.png` } const PLUGIN_SIMPLE_TITLES: Record = { claude_code: "Remembers your code conventions and decisions", codex: "Codex sessions that remember your project", cursor: "Cursor sessions with persistent project memory", opencode: "OpenCode with persistent project memory", openclaw: "Save chats from Telegram, Discord and Slack", hermes: "Persistent memory for the Hermes agent", } type CategoryFilter = | "all" | "connected" | "plugins" | "knowledge-bases" | "apps-extensions" | "ai-clients" const CATEGORY_VALUES: readonly CategoryFilter[] = [ "all", "connected", "ai-clients", "plugins", "knowledge-bases", "apps-extensions", ] as const const catParam = parseAsStringEnum([ ...CATEGORY_VALUES, ]).withDefault("all") const CATEGORY_LABEL: Record = { all: "All", connected: "Active", plugins: "Plugins", "knowledge-bases": "Knowledge bases", "apps-extensions": "Apps & extensions", "ai-clients": "MCP", } const SECTION_ORDER: Array> = [ "ai-clients", "plugins", "knowledge-bases", "apps-extensions", ] function itemCategory( item: Item, ): Exclude { switch (item.kind) { case "plugin": return "plugins" case "connector": return "knowledge-bases" case "mcp-client": return "ai-clients" case "client": case "import": return "apps-extensions" } } interface BaseItem { id: string name: string tagline: string icon: ReactNode docsUrl?: string pro?: boolean max?: boolean kind: ItemKind simpleTitle?: string dev?: boolean isNew?: boolean } interface PluginItem extends BaseItem { kind: "plugin" pluginId: string } interface ConnectorItem extends BaseItem { kind: "connector" provider: ConnectorProvider } interface ClientItem extends BaseItem { kind: "client" action: | { type: "external"; href: string } | { type: "view"; viewMode: ViewParamValue } } interface MCPClientItem extends BaseItem { kind: "mcp-client" clientKey: MCPClientKey } interface ImportItem extends BaseItem { kind: "import" viewMode: ViewParamValue } type Item = PluginItem | ConnectorItem | ClientItem | MCPClientItem | ImportItem const SECTIONS: Array<{ label: string items: (plugin: typeof PLUGIN_CATALOG) => Item[] }> = [ { label: "MCP", items: () => MCP_CLIENTS.map((c) => ({ kind: "mcp-client", clientKey: c.key, id: `mcp-${c.key}`, name: c.name, tagline: c.tagline, simpleTitle: c.simpleTitle, dev: c.dev, icon: c.key === "mcp-url" ? ( ) : ( {c.name} ), docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction", })), }, { label: "Plugins", items: (catalog) => Object.keys(catalog).map((id) => { const plugin = catalog[id] if (!plugin) throw new Error(`Missing plugin ${id}`) return { kind: "plugin", id: `plugin-${id}`, pluginId: id, name: plugin.name, tagline: plugin.tagline, icon: ( {plugin.name} ), docsUrl: plugin.docsUrl, pro: !FREE_TIER_PLUGIN_IDS.includes(id), simpleTitle: PLUGIN_SIMPLE_TITLES[id], dev: true, } }), }, { label: "Knowledge bases", items: () => [ { kind: "connector", id: "google-drive", provider: "google-drive", name: "Google Drive", tagline: "Sync Docs, Sheets and Slides into your memory", simpleTitle: "Your Docs, Sheets and Slides, searchable", icon: , pro: true, docsUrl: "https://supermemory.ai/docs/connectors/google-drive", }, { kind: "connector", id: "notion", provider: "notion", name: "Notion", tagline: "Import Notion pages and databases", simpleTitle: "All your Notion pages, in supermemory", icon: , pro: true, docsUrl: "https://supermemory.ai/docs/connectors/notion", }, { kind: "connector", id: "onedrive", provider: "onedrive", name: "OneDrive", tagline: "Bring in Office documents from OneDrive", simpleTitle: "Your OneDrive files, ready to recall", icon: , pro: true, docsUrl: "https://supermemory.ai/docs/connectors/onedrive", }, { kind: "connector", id: "granola", provider: "granola", name: "Granola", tagline: "Sync AI meeting notes into your memory", simpleTitle: "Your meeting notes, ready to recall", icon: , pro: true, docsUrl: "https://supermemory.ai/docs/connectors/granola", }, ], }, { label: "Apps & extensions", items: () => [ { kind: "client", id: "chrome", name: "Chrome Extension", tagline: "Save webpages, import bookmarks, sync ChatGPT memories", simpleTitle: "Save any webpage with one click", icon: , action: { type: "external", href: CHROME_EXTENSION_URL }, }, { kind: "client", id: "poke", name: "Poke", tagline: "Recall and save memories from Poke over text", simpleTitle: "Text Poke to recall and save your memories", isNew: true, icon: (
Poke
), action: { type: "external", href: POKE_RECIPE_URL }, }, { kind: "import", id: "x-bookmarks", name: "Import X bookmarks", tagline: "Turn your X/Twitter bookmarks into memories", simpleTitle: "Turn your X bookmarks into memory", icon: X, viewMode: "import" as ViewParamValue, }, { kind: "client", id: "raycast", name: "Raycast", tagline: "Add and search memories from Raycast on Mac", simpleTitle: "Save and search from Raycast on Mac", icon: , action: { type: "view", viewMode: "raycast" as ViewParamValue }, dev: true, }, { kind: "client", id: "shortcuts", name: "Apple Shortcuts", tagline: "Add memories from iPhone, iPad or Mac", simpleTitle: "Save anything from your phone or Mac", icon: , action: { type: "view", viewMode: "shortcuts" as ViewParamValue }, }, ], }, ] export function DetailWrapper({ onBack, children, }: { onBack: () => void children: ReactNode }) { return (
{children}
) } function ProChip({ children = "Pro" }: { children?: ReactNode }) { return ( {children} ) } function NewChip() { return ( New ) } function IconBox({ children, size = "md", }: { children: ReactNode size?: "sm" | "md" }) { return (
{children}
) } const PLUGIN_COMMANDS: InstallStep[] = [ { code: "npx supermemory plugin", copyLabel: "Install plugins", title: "Install plugins", description: "Detect Claude Code, Cursor, OpenCode, and Codex, install your selections, then approve OAuth once in the browser.", }, { code: "npx supermemory plugin login", copyLabel: "Reconnect plugins", title: "Reconnect plugins", description: "Run browser OAuth again for plugins that are already installed, without reinstalling them.", }, { code: "npx supermemory plugin uninstall", copyLabel: "Uninstall plugins", title: "Uninstall plugins", description: "Remove selected plugin integrations while keeping your credentials and memories.", }, ] const PLUGIN_COMMAND_CLIENTS = [ "claude_code", "cursor", "codex", "opencode", ] as const type PluginSetupTab = "agent" | "manual" const PLUGIN_CLI_TARGETS: Partial> = { claude_code: "claude", codex: "codex", cursor: "cursor", opencode: "opencode", } function pluginAgentPrompt(plugin: PluginInfo): string { const cliTarget = PLUGIN_CLI_TARGETS[plugin.id] if (cliTarget) { return `Install and connect the Supermemory plugin for ${plugin.name} on this machine. Run \`npx supermemory plugin --only ${cliTarget}\`, complete the browser OAuth flow when it opens, then verify the plugin is installed and authenticated.` } const docsInstruction = plugin.docsUrl ? ` Follow the official setup instructions at ${plugin.docsUrl}.` : " Follow its official setup instructions." return `Install and connect the Supermemory integration for ${plugin.name} on this machine.${docsInstruction} Complete authentication securely, then verify the integration is working.` } function PluginSetupMethodTabs({ value, onChange, }: { value: PluginSetupTab onChange: (value: PluginSetupTab) => void }) { return (
{(["agent", "manual"] as const).map((tab) => ( ))}
) } function PluginAgentInstructions({ plugin }: { plugin: PluginInfo }) { const prompt = pluginAgentPrompt(plugin) return (

{prompt}

) } function PluginCommandsDialog({ open, onOpenChange, }: { open: boolean onOpenChange: (open: boolean) => void }) { return ( Supermemory plugin commands
{PLUGIN_COMMAND_CLIENTS.map((pluginId) => { const plugin = PLUGIN_CATALOG[pluginId] if (!plugin) return null return ( ) })}

Plugin commands

Install, reconnect, or remove integrations from one CLI.

Run these commands from your terminal.

) } type InfoUseCase = { title: string description: string } type InfoModalCloseReason = Parameters< typeof analytics.integrationInfoModalClosed >[0]["close_reason"] const MCP_INFO_USE_CASES: InfoUseCase[] = [ { title: "Persistent assistant memory", description: "Store useful context during conversations and recall it later from this MCP client.", }, { title: "Shared context across tools", description: "Use the same Supermemory account across MCP-compatible clients so memory follows the user between sessions.", }, { title: "Profiles and project context", description: "Bring user profiles and project-scoped memories into supported AI clients when they need context.", }, ] const ITEM_INFO_USE_CASES: Record = { "plugin-claude_code": [ { title: "Session context injection", description: "Fetch relevant project memories, user preferences, and past interactions when Claude Code starts a session.", }, { title: "Automatic coding capture", description: "Save useful tool activity like edits, new files, shell commands, and spawned tasks for future sessions.", }, ], "plugin-codex": [ { title: "Recall before each prompt", description: "Inject relevant memories and profile context into Codex before each prompt.", }, { title: "Capture after sessions", description: "Store conversation transcripts after a session, scoped to the current project and user.", }, { title: "Explicit memory skills", description: "Use supermemory-search, supermemory-save, and supermemory-forget when memory needs direct control.", }, ], "plugin-opencode": [ { title: "Project memory in OpenCode", description: "Inject preferences, project knowledge, and past interactions at the start of OpenCode sessions.", }, { title: "Smart session capture", description: "Save memories from explicit phrases like remember or save this, and summarize long sessions during compaction.", }, ], "plugin-openclaw": [ { title: "Memory across messaging channels", description: "Give OpenClaw memory across WhatsApp, Telegram, Discord, Slack, iMessage, and other channels.", }, { title: "Auto-recall and auto-capture", description: "Inject relevant memories before AI turns and store conversation exchanges after turns.", }, { title: "Direct memory tools", description: "Let the AI store, search, forget, and inspect profile memories during conversations.", }, ], "plugin-hermes": [ { title: "Semantic memory for Hermes", description: "Add long-term memory, profile recall, search, and session-aware ingest to Hermes.", }, { title: "Turn and session memory", description: "Prefetch relevant context before turns, capture completed turns, and ingest full sessions for richer graph updates.", }, { title: "Organized containers", description: "Use profile-scoped memory and optional multi-container tags for work, personal, or project-specific context.", }, ], "google-drive": [ { title: "Scoped Drive sync", description: "Sync selected Google Docs, Sheets, Slides, and PDFs after OAuth and the hosted file picker.", }, { title: "Fresh knowledge base", description: "Keep selected Drive files updated in Supermemory, with scheduled and manual import support.", }, ], notion: [ { title: "Workspace knowledge sync", description: "Sync Notion pages, databases, and blocks into Supermemory from connected workspaces.", }, { title: "Rich Notion context", description: "Preserve rich formatting and database properties so Notion content remains useful for retrieval.", }, ], onedrive: [ { title: "Microsoft 365 documents", description: "Sync Word documents, Excel spreadsheets, and PowerPoint presentations from OneDrive.", }, { title: "Personal and business accounts", description: "Connect personal or business OneDrive accounts and keep Office files updated through sync.", }, ], chrome: [ { title: "Save from the browser", description: "Capture webpages into Supermemory while browsing instead of manually copying content.", }, { title: "Bring bookmarks into memory", description: "Import saved browser context so it can be searched and reused later.", }, ], shortcuts: [ { title: "Quick mobile capture", description: "Add memories from iPhone, iPad, or Mac through Apple Shortcuts.", }, { title: "Save without opening the app", description: "Send useful snippets and links into Supermemory from native Apple workflows.", }, ], raycast: [ { title: "Fast desktop capture", description: "Add memories from Raycast on Mac without leaving the launcher.", }, { title: "Search from Raycast", description: "Look up Supermemory content directly from your desktop command bar.", }, ], "x-bookmarks": [ { title: "Import saved X posts", description: "Turn X/Twitter bookmarks into searchable Supermemory memories.", }, { title: "Reuse social research", description: "Bring bookmarked threads, references, and ideas into the same memory layer as your other tools.", }, ], } function getInfoUseCases(id: string): InfoUseCase[] { return ITEM_INFO_USE_CASES[id] ?? MCP_INFO_USE_CASES } function ItemInfoButton({ name, onClick, }: { name: string onClick: () => void }) { return ( ) } function ItemInfoDialog({ actionSlot, docsUrl, hideDismiss, icon, id, kind, name, onOpenChange, open, }: { actionSlot: ReactNode docsUrl?: string hideDismiss?: boolean icon: ReactNode id: string kind: ItemKind name: string onOpenChange: (open: boolean) => void open: boolean }) { const useCases = getInfoUseCases(id) const closeWithReason = (closeReason: InfoModalCloseReason) => { analytics.integrationInfoModalClosed({ kind, id, name, close_reason: closeReason, }) onOpenChange(false) } return ( { if (nextOpen) { onOpenChange(true) return } closeWithReason("dismiss") }} > e.stopPropagation()} onClick={(e) => { e.stopPropagation() closeWithReason("dismiss") }} className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 backdrop-blur-[4px]" /> e.stopPropagation()} onInteractOutside={(e) => e.preventDefault()} style={{ boxShadow: "0 2.842px 14.211px 0 rgba(0,0,0,0.25), 0.711px 0.711px 0.711px 0 rgba(255,255,255,0.10) inset", }} className={cn( dmSans125ClassName(), "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] duration-100", "flex max-h-[88dvh] flex-col gap-3 overflow-hidden border border-white/[0.12] bg-[#1B1F24] p-0 px-3 pt-3 pb-4 text-[#FAFAFA] rounded-2xl md:px-4 sm:max-w-[560px] sm:rounded-[22px]", )} > {name} use cases and docs
{icon}

{name}

Use cases that apply to this Supermemory connection.

{docsUrl && ( Docs )}
{useCases.map((useCase, index) => (
{index + 1} {index < useCases.length - 1 && ( )}

{useCase.title}

{useCase.description}

))}
{!hideDismiss && ( )} {/* biome-ignore lint/a11y/noStaticElementInteractions: closes the info dialog after the nested action button runs. */} {/* biome-ignore lint/a11y/useKeyWithClickEvents: keyboard handling stays on the nested real button. */}
closeWithReason("action")}>{actionSlot}
) } function DisconnectButton({ onConfirm }: { onConfirm: () => void }) { const [confirming, setConfirming] = useState(false) useEffect(() => { if (!confirming) return const t = setTimeout(() => setConfirming(false), 3000) return () => clearTimeout(t) }, [confirming]) return ( ) } function ActiveButton({ count, lastActive, onClick, }: { count: number lastActive?: string | null onClick: () => void }) { return ( ) } function McpConnectedPill({ connectedAt, lastActive, }: { connectedAt?: string | null lastActive?: string | null }) { return ( Connected {(lastActive ?? connectedAt) && ( · {formatRelativeTime(lastActive ?? connectedAt)} )} ) } function FinishSetupButton({ onClick }: { onClick: () => void }) { return ( Finish setup ) } function ConnectionsCountPill({ count }: { count: number }) { return ( {count > 1 ? `${count} connected` : "Connected"} ) } function ImportedPill({ lastImportedAt, }: { lastImportedAt: string | number | Date | null }) { return ( Imported {lastImportedAt && ( · {formatRelativeTime(lastImportedAt)} )} ) } const CONNECTOR_META: Record< ConnectorProvider, { name: string; icon: ReactNode; documentLabel: string } > = { "google-drive": { name: "Google Drive", icon: , documentLabel: "documents", }, notion: { name: "Notion", icon: , documentLabel: "pages", }, onedrive: { name: "OneDrive", icon: , documentLabel: "documents", }, granola: { name: "Granola", icon: , documentLabel: "notes", }, } interface PluginEntry { kind: "plugin" id: string name: string icon: ReactNode pro: boolean agentCount: number createdAt: string | null lastActive: string | null onManage: () => void } interface ConnectorEntry { kind: "connector" id: string name: string documentLabel: string icon: ReactNode pro: boolean provider: ConnectorProvider connection: Connection connectionCount: number email: string | null spaceName: string | null createdAt: string | null onManage: () => void onReconnect: () => void } interface McpEntry { kind: "mcp" id: string name: string icon: ReactNode connectionCount: number createdAt: string | null lastActive: string | null onManage: () => void } type RailEntry = PluginEntry | ConnectorEntry | McpEntry function railConnectionMeta(connection: Connection) { const m = connection.metadata as Record | undefined return { syncInProgress: m?.syncInProgress === true, lastSyncedAt: typeof m?.lastSyncedAt === "number" ? m.lastSyncedAt : undefined, documentCount: typeof m?.documentCount === "number" ? m.documentCount : 0, } } function RailDetail({ label, value }: { label: string; value: ReactNode }) { return (
{label} {value}
) } function RailAction({ label, onClick, danger, }: { label: string onClick: () => void danger?: boolean }) { return ( ) } function RailRow({ icon, name, statusLine, expanded, onToggle, children, }: { icon: ReactNode name: string statusLine: ReactNode expanded: boolean onToggle: () => void children: ReactNode }) { return (
{expanded && (
{children}
)}
) } function ActiveStatusDot() { return ( <> Active ) } function PluginRailRow({ entry }: { entry: PluginEntry }) { const [expanded, setExpanded] = useState(false) const lastTime = entry.lastActive ?? entry.createdAt const suffix = [ entry.agentCount > 1 ? `${entry.agentCount} agents` : null, lastTime ? formatRelativeTime(lastTime) : null, ] .filter(Boolean) .join(" · ") return ( setExpanded((v) => !v)} statusLine={
{suffix && ( · {suffix} )}
} > {entry.createdAt && ( )} {entry.lastActive && ( )}
) } const CONNECTOR_STATUS = { expired: { color: "#EF4444", label: "Expired" }, syncing: { color: "#4BA0FA", label: "Syncing" }, synced: { color: "#00AC3F", label: "Synced" }, idle: { color: "#737373", label: "Connected" }, } as const function ConnectorRailRow({ entry }: { entry: ConnectorEntry }) { const [expanded, setExpanded] = useState(false) const { needsReauth } = useConnectionHealth(entry.connection.id) const meta = railConnectionMeta(entry.connection) const status: keyof typeof CONNECTOR_STATUS = needsReauth ? "expired" : meta.syncInProgress ? "syncing" : meta.lastSyncedAt ? "synced" : "idle" const { color, label } = CONNECTOR_STATUS[status] const statusParts = [ status !== "syncing" && meta.documentCount > 0 ? String(meta.documentCount) : null, status === "synced" && meta.lastSyncedAt ? compactRelativeTime(meta.lastSyncedAt) : null, ].filter(Boolean) return ( setExpanded((v) => !v)} statusLine={
{label} {statusParts.length > 0 && ( · {statusParts.join(" · ")} )}
} > {entry.email && } {entry.createdAt && ( )} {meta.lastSyncedAt && ( )} {meta.documentCount > 0 && ( )} {entry.spaceName && } {needsReauth && ( Reconnect needed
} /> )}
{needsReauth && ( )}
) } function McpRailRow({ entry }: { entry: McpEntry }) { const [expanded, setExpanded] = useState(false) const lastTime = entry.lastActive ?? entry.createdAt const suffix = [ entry.connectionCount > 1 ? `${entry.connectionCount} connections` : null, lastTime ? formatRelativeTime(lastTime) : null, ] .filter(Boolean) .join(" · ") return ( setExpanded((v) => !v)} statusLine={
{suffix && ( · {suffix} )}
} > {entry.createdAt && ( )} {entry.lastActive && ( )}
) } const SKELETON_KEYS = ["s1", "s2", "s3", "s4", "s5"] function RailSkeleton({ rows }: { rows: number }) { return (
{SKELETON_KEYS.slice(0, rows).map((k) => (
))}
) } function RailEmpty({ icon, title, hint, }: { icon: ReactNode title: string hint: string }) { return (
{icon}

{title}

{hint}

) } function ActiveConnectionsRail({ entries, loading, className, }: { entries: RailEntry[] loading?: boolean className?: string }) { return ( ) } type RecentDoc = z.infer< typeof DocumentsWithMemoriesResponseSchema >["documents"][number] function hostnameOf(url: string | null | undefined): string | null { if (!url) return null try { return new URL(url).hostname.replace(/^www\./, "") } catch { return null } } const CONNECTOR_SMALL_ICON: Record = { "google-drive": , notion: , onedrive: , granola: , } function pluginIconNode(iconSrc: string | null): ReactNode { if (!iconSrc) return return ( ) } function resolveDocSource( doc: RecentDoc, connectionSource: Map, ): { label: string; icon: ReactNode } { const tags = (doc as { containerTags?: unknown }).containerTags if (Array.isArray(tags)) { for (const tag of tags) { if (typeof tag !== "string") continue const space = detectPluginSpace(tag) if (space) { return { label: space.label, icon: pluginIconNode(space.iconSrc) } } } } if (doc.connectionId) { const provider = connectionSource.get(doc.connectionId) if (provider) { return { label: CONNECTOR_META[provider].name, icon: CONNECTOR_SMALL_ICON[provider], } } } const cc = detectPluginSource( doc.metadata as Record | null | undefined, doc.source, ) if (cc) { return { label: cc.label, icon: pluginIconNode(cc.iconSrc) } } if (doc.source === "mcp") { return { label: "MCP", icon: } } const type = (doc.type ?? "").toLowerCase() if (type.includes("notion")) { return { label: "Notion", icon: } } if ( type.includes("google") || type.includes("gdrive") || type.includes("drive") ) { return { label: "Google Drive", icon: } } if (type.includes("onedrive") || type.includes("microsoft")) { return { label: "OneDrive", icon: } } if (type.includes("granola")) { return { label: "Granola", icon: } } const host = hostnameOf(doc.url) if (host) { return { label: host, icon: } } return { label: "Note", icon: , } } function docDisplayTitle(doc: RecentDoc, sourceLabel: string): string { const t = doc.title?.trim() if (t && !/^untitled/i.test(t)) return t const summary = typeof doc.summary === "string" ? doc.summary.trim() : "" if (summary) { const line = summary .split("\n") .find((l) => l.trim()) ?.trim() if (line) return line.length > 80 ? `${line.slice(0, 79)}…` : line } return `${sourceLabel} session` } function RecentDocRow({ doc, connectionSource, onOpen, }: { doc: RecentDoc connectionSource: Map onOpen: () => void }) { const { label, icon } = resolveDocSource(doc, connectionSource) const title = docDisplayTitle(doc, label) return ( ) } function RecentlyAddedCard({ docs, connectionSource, loading, onOpenDoc, onViewAll, className, }: { docs: RecentDoc[] connectionSource: Map loading?: boolean onOpenDoc: (doc: RecentDoc) => void onViewAll: () => void className?: string }) { return ( ) } type MobileActivityTab = "active" | "recent" function MobileActivityPanel({ entries, docs, connectionSource, activeLoading, recentsLoading, onOpenDoc, onViewAll, }: { entries: RailEntry[] docs: RecentDoc[] connectionSource: Map activeLoading?: boolean recentsLoading?: boolean onOpenDoc: (doc: RecentDoc) => void onViewAll: () => void }) { const [tab, setTab] = useState("active") const hasActiveTab = activeLoading || entries.length > 0 const hasRecentTab = recentsLoading || docs.length > 0 const showTabs = hasActiveTab && hasRecentTab const activeTab: MobileActivityTab = hasActiveTab && (tab === "active" || !hasRecentTab) ? "active" : "recent" useEffect(() => { if (!hasActiveTab && hasRecentTab && tab === "active") { setTab("recent") } if (!hasRecentTab && hasActiveTab && tab === "recent") { setTab("active") } }, [hasActiveTab, hasRecentTab, tab]) const tabClass = (value: MobileActivityTab) => cn( dmSans125ClassName(), "flex h-8 flex-1 items-center justify-center gap-1.5 rounded-full px-3 text-[12px] font-medium transition-colors", activeTab === value ? "bg-white/[0.10] text-[#FAFAFA]" : "text-[#A1A1AA] hover:text-[#FAFAFA]", ) return ( ) } function ItemCard({ actionSlot, infoActionSlot, icon, id, kind, name, tagline, pro, max, isNew, docsUrl, leftIndicator, statusSlot, layoutClassName, }: { actionSlot: ReactNode infoActionSlot?: ReactNode icon: ReactNode id: string kind: ItemKind name: string tagline: string pro?: boolean max?: boolean isNew?: boolean docsUrl?: string leftIndicator?: ReactNode statusSlot?: ReactNode layoutClassName?: string }) { const [infoOpen, setInfoOpen] = useState(false) return ( // biome-ignore lint/a11y/useSemanticElements: the card contains nested action buttons, so it cannot be a native button.
setInfoOpen(true)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault() setInfoOpen(true) } }} className={cn( "group relative flex h-full cursor-pointer flex-row items-center gap-2.5 rounded-[10px] bg-[#14161A] px-2.5 py-2 transition-colors hover:bg-[#16181D] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4BA0FA]/45 sm:flex-col sm:items-stretch sm:gap-4 sm:rounded-[12px] sm:p-4", "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]", id === "shortcuts" && "max-sm:grid max-sm:grid-cols-[auto_minmax(0,1fr)] max-sm:items-center", layoutClassName, )} > setInfoOpen(true)} />
{icon}
{leftIndicator} {name} {isNew && } {max ? Max : pro && }
{/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the status action. */}
e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} > {statusSlot}
{/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the primary action. */}
button]:!h-7 [&>button]:!min-w-[82px] [&>button]:!px-3 [&>button]:!text-[11px] sm:[&>button]:!h-9 sm:[&>button]:!min-w-[116px] sm:[&>button]:!px-5 sm:[&>button]:!text-[14px]", id === "shortcuts" && "max-sm:w-full max-sm:shrink max-sm:[&>div]:w-full", )} onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} > {actionSlot}
) } interface FeaturedPick { id: string name: string emoji?: string headline: string support: string tagline: string icon: ReactNode backdrop?: ReactNode docsUrl?: string ctaLabel: string onCta: () => void } const FEATURED_ROTATE_MS = 7000 function FeaturedHero({ picks }: { picks: FeaturedPick[] }) { const [index, setIndex] = useState(0) const [paused, setPaused] = useState(false) const [manualOverride, setManualOverride] = useState(false) useEffect(() => { if (picks.length <= 1) return if (paused || manualOverride) return const t = setInterval(() => { setIndex((i) => (i + 1) % picks.length) }, FEATURED_ROTATE_MS) return () => clearInterval(t) }, [picks.length, paused, manualOverride]) if (picks.length === 0) return null const pick = picks[index] ?? picks[0] if (!pick) return null return ( // biome-ignore lint/a11y/useSemanticElements: card wraps nested dot buttons, so it can't be a
)}

{pick.headline}

{pick.emoji ? `${pick.emoji} ` : ""} {pick.name} {" "} · {pick.support}

) } function SearchToggle({ value, onChange, expanded, setExpanded, }: { value: string onChange: (next: string) => void expanded: boolean setExpanded: (next: boolean) => void }) { const inputRef = useRef(null) const open = () => { setExpanded(true) requestAnimationFrame(() => inputRef.current?.focus()) } const close = () => { onChange("") setExpanded(false) } if (!expanded && !value) { return ( ) } return (
onChange(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") close() }} onBlur={() => { if (!value) setExpanded(false) }} placeholder="Search integrations" className={cn( dmSans125ClassName(), "min-w-0 flex-1 bg-transparent text-[12px] text-[#FAFAFA] placeholder:text-[#525D6E] focus:outline-none", )} /> {value && ( )}
) } function CategoryFilterToggle({ value, onChange, counts, compact, }: { value: CategoryFilter onChange: (next: CategoryFilter) => void counts: Record compact?: boolean }) { const visible = compact ? [value] : CATEGORY_VALUES.filter((v) => v === "all" || counts[v] > 0) return (
{visible.map((v) => { const active = value === v return ( ) })}
) } export function IntegrationsView({ publicMode = false, onOpenDocument, }: { publicMode?: boolean onOpenDocument?: (doc: RecentDoc) => void }) { const { setViewMode } = useViewMode() const queryClient = useQueryClient() const { org } = useAuth() const { allProjects } = useContainerTags() const shortcutsConnect = useShortcutsConnect() const autumn = useCustomer({ queryOptions: { enabled: !publicMode } }) const promoCode = usePromoCode() // connectorAccess covers pro-tier connectors (incl. company_brain orgs); plugins // stay on hasProProduct. See useConnectorAccess. const { hasPro: hasProProduct, connectorAccess } = useConnectorAccess({ enabled: !publicMode, }) const isAutumnLoading = !publicMode && autumn.isLoading const [connectingPlugin, setConnectingPlugin] = useState(null) const [connectingProvider, setConnectingProvider] = useState(null) const [granolaModalOpen, setGranolaModalOpen] = useState(false) const [pluginCommandsOpen, setPluginCommandsOpen] = useState(false) const [newKey, setNewKey] = useState<{ open: boolean key: string pluginId: string | null loading: boolean }>({ open: false, key: "", pluginId: null, loading: false }) const [pluginSetupTab, setPluginSetupTab] = useState("agent") const openPluginSetup = useCallback((pluginId: string) => { setPluginSetupTab("agent") setNewKey({ open: true, key: "", pluginId, loading: false }) }, []) const [connectedPluginId, setConnectedPluginId] = useState( null, ) const { data: pluginsData } = useQuery({ queryFn: async () => { const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const res = await fetch(`${API_URL}/v3/auth/plugins`, { credentials: "include", }) if (!res.ok) throw new Error("Failed to fetch plugins") return (await res.json()) as { plugins: string[] } }, enabled: !publicMode, queryKey: ["plugins"], }) const { data: connections = [], isLoading: connectionsLoading } = useQuery({ queryKey: ["connections"], queryFn: async () => { const response = await $fetch("@post/connections/list", { body: { containerTags: [] }, }) if (response.error) throw new Error(response.error?.message || "Failed to load connections") return response.data as Connection[] }, staleTime: 30 * 1000, enabled: !publicMode && connectorAccess, }) const { data: apiKeys = [], refetch: refetchKeys, isLoading: apiKeysLoading, } = useQuery({ queryKey: ["api-keys", org?.id], queryFn: async () => { if (!org?.id) return [] const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const res = await fetch(`${API_URL}/v3/auth/keys`, { credentials: "include", }) if (!res.ok) return [] const data = (await res.json()) as { keys?: ListedApiKey[] } return data.keys ?? [] }, enabled: !publicMode && !!org?.id, staleTime: 30 * 1000, }) const { data: xBookmarksImport } = useQuery({ queryKey: ["x-bookmarks-import-status"], queryFn: async () => { const response = await $fetch("@post/documents/documents", { body: { page: 1, limit: 1, sort: "createdAt", order: "desc", categories: ["tweet"], }, disableValidation: true, }) if (response.error) throw new Error( response.error?.message || "Failed to load X bookmarks status", ) return { count: response.data?.pagination?.totalItems ?? 0, lastImportedAt: response.data?.documents?.[0]?.createdAt ?? null, } }, staleTime: 5 * 60 * 1000, enabled: !publicMode, }) const tweetCount = xBookmarksImport?.count ?? 0 const lastTweetImportAt = xBookmarksImport?.lastImportedAt ?? null const keyPrefix = useCallback((key: ListedApiKey): string | null => { return key.start ?? (key.name?.startsWith("sm_") ? key.name : null) }, []) const { active: activePlugins, setup: setupPlugins } = useMemo( () => parsePluginAuthKeys(apiKeys, keyPrefix), [apiKeys, keyPrefix], ) const activeMcpKeys = useMemo( () => parseMcpAuthKeys(apiKeys, keyPrefix), [apiKeys, keyPrefix], ) const activePluginById = useMemo(() => { const map = new Map() for (const key of activePlugins) { const existing = map.get(key.pluginId) if (!existing) { map.set(key.pluginId, key) continue } const a = key.lastRequest ? new Date(key.lastRequest).getTime() : 0 const b = existing.lastRequest ? new Date(existing.lastRequest).getTime() : 0 if (a >= b) map.set(key.pluginId, key) } return map }, [activePlugins]) const activeMcpKey = useMemo(() => { let latest: ConnectedMcpKey | null = null for (const key of activeMcpKeys) { if (!latest) { latest = key continue } const a = toMs(key.lastRequest ?? key.createdAt) const b = toMs(latest.lastRequest ?? latest.createdAt) if (a >= b) latest = key } return latest }, [activeMcpKeys]) const activeCountByPlugin = useMemo(() => { const map = new Map() for (const key of activePlugins) { map.set(key.pluginId, (map.get(key.pluginId) ?? 0) + 1) } return map }, [activePlugins]) const setupPluginIds = useMemo( () => new Set(setupPlugins.map((k) => k.pluginId)), [setupPlugins], ) const connectionsByProvider = useMemo(() => { const out: Record = { "google-drive": [], notion: [], onedrive: [], granola: [], } for (const c of connections) { const p = c.provider as ConnectorProvider if (p in out) out[p].push(c) } return out }, [connections]) const connectionSource = useMemo(() => { const m = new Map() for (const c of connections) { const p = c.provider as ConnectorProvider if (p in CONNECTOR_META) m.set(c.id, p) } return m }, [connections]) const createPluginKeyMutation = useMutation({ mutationFn: async (pluginId: string) => { const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const params = new URLSearchParams({ client: pluginId }) const res = await fetch(`${API_URL}/v3/auth/key?${params}`, { credentials: "include", }) if (!res.ok) { const errorData = (await res.json().catch(() => ({}))) as { message?: string } throw new Error(errorData.message || "Failed to create plugin key") } return (await res.json()) as { key: string } }, onMutate: (pluginId) => setConnectingPlugin(pluginId), onError: (err) => { setNewKey((s) => ({ ...s, loading: false })) toast.error("Failed to connect plugin", { description: err instanceof Error ? err.message : "Unknown error", }) }, onSettled: () => { setConnectingPlugin(null) queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id] }) }, onSuccess: (data, pluginId) => { setNewKey((s) => s.open && s.pluginId === pluginId ? { ...s, key: data.key, loading: false } : s, ) }, }) const generatePluginKey = () => { const pluginId = newKey.pluginId if ( !pluginId || newKey.key || newKey.loading || createPluginKeyMutation.isPending ) return setNewKey((s) => ({ ...s, loading: true })) createPluginKeyMutation.mutate(pluginId) } const selectPluginSetupTab = (tab: PluginSetupTab) => { setPluginSetupTab(tab) if (tab === "manual") generatePluginKey() } const addConnectionMutation = useMutation({ mutationFn: async (provider: ConnectorProvider) => { const response = await $fetch("@post/connections/:provider", { params: { provider }, body: { redirectUrl: window.location.href, containerTags: [], }, }) if ("data" in response && response.data && !("error" in response.data)) { return response.data } throw new Error(response.error?.message || "Failed to connect") }, onMutate: (provider) => { setConnectingProvider(provider) analytics.connectionAuthStarted({ provider }) }, onError: (err) => { setConnectingProvider(null) toast.error("Failed to connect", { description: err instanceof Error ? err.message : "Unknown error", }) }, onSuccess: (data) => { if (data?.authLink) { window.location.href = data.authLink return } setConnectingProvider(null) toast.error("Connect link missing — try again.") }, }) const handleRevokePluginKey = async (keyId: string) => { try { await authClient.apiKey.delete({ keyId }) toast.success("Plugin disconnected") refetchKeys() } catch { toast.error("Failed to disconnect plugin") } } const handleUpgrade = useCallback( async (planId?: unknown) => { const checkoutPlanId = planId === "api_max" ? "api_max" : "api_pro" try { const result = await autumn.attach({ planId: checkoutPlanId, discounts: promoCode.getDiscounts(), successUrl: `${window.location.origin}/integrations`, }) promoCode.clear() if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") return } autumn.refetch?.() } catch (error) { console.error(error) toast.error("Failed to start checkout. Please try again.") } }, [autumn, promoCode], ) const redirectToLogin = useCallback(() => { const loginUrl = new URL("/login", window.location.origin) loginUrl.searchParams.set("redirect", window.location.href) window.location.assign(loginUrl.toString()) }, []) const availablePluginIds = publicMode ? Object.keys(PLUGIN_CATALOG) : (pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG)) const enabledPluginIds = new Set( availablePluginIds.filter((id) => PLUGIN_CATALOG[id]), ) const [category, setCategory] = useQueryState("cat", catParam) const [, setAddDoc] = useQueryState("add", addDocumentParam) const [, setDocId] = useQueryState("doc", docParam) const [mcpClient, setMcpClient] = useQueryState("mcpClient", parseAsString) const [mcpModalOpen, setMcpModalOpen] = useState(false) const [search, setSearch] = useState("") const [searchExpanded, setSearchExpanded] = useState(false) const openMcpClient = (key: MCPClientKey) => { void setMcpClient(key) setMcpModalOpen(true) } // Deeplink: /integrations?connect= auto-opens that card's connect flow. const [connectTarget, setConnectTarget] = useQueryState( "connect", parseAsString, ) // Tracks the last target we acted on; reset when the param clears so a fresh deeplink re-fires. const connectHandledRef = useRef(null) useEffect(() => { if (!connectTarget) { connectHandledRef.current = null return } if (connectHandledRef.current === connectTarget) return const target = connectTarget const isPlugin = !!PLUGIN_CATALOG[target] const freeTier = isPlugin && isFreeTierPlugin(target) // Paid plugins and granola need the plan query before deciding upgrade-vs-connect. const needsPlan = (isPlugin && !freeTier) || target === "granola" if (needsPlan && isAutumnLoading) return // Defer to a macrotask and cancel on cleanup so React Strict Mode's mount→unmount→remount // fires this exactly once (on the surviving mount) instead of opening/minting twice. let cancelled = false const timer = setTimeout(() => { if (cancelled) return connectHandledRef.current = target if (publicMode) { redirectToLogin() return } if (isPlugin) { if (!freeTier && !hasProProduct) { void setConnectTarget(null) handleUpgrade("api_pro") } else { openPluginSetup(target) } return } if (target === "granola") { if (!connectorAccess) { void setConnectTarget(null) handleUpgrade("api_pro") } else { setGranolaModalOpen(true) } return } if (["notion", "google-drive", "onedrive"].includes(target)) { // The add-document modal is driven by its own ?add param, so clearing ?connect is safe. void setConnectTarget(null) void setAddDoc("connect") } }, 0) return () => { cancelled = true clearTimeout(timer) } }, [ connectTarget, isAutumnLoading, hasProProduct, connectorAccess, publicMode, redirectToLogin, setConnectTarget, setAddDoc, handleUpgrade, openPluginSetup, ]) const closeMcpModal = () => { setMcpModalOpen(false) void setMcpClient(null) } const activeMcpClient = mcpClient ? MCP_CLIENTS.find((c) => c.key === mcpClient) : undefined const allItems = useMemo( () => SECTIONS.flatMap((s) => s.items(PLUGIN_CATALOG)).filter( (item) => item.kind !== "plugin" || enabledPluginIds.has(item.pluginId), ), [enabledPluginIds], ) const isItemConnected = useCallback( (item: Item): boolean => { if (publicMode) return false if (item.kind === "plugin") { return activePluginById.has(item.pluginId) } if (item.kind === "connector") { return connectionsByProvider[item.provider].length > 0 } if (item.kind === "import") { return tweetCount > 0 } return false }, [activePluginById, connectionsByProvider, publicMode, tweetCount], ) const counts = useMemo>( () => ({ all: allItems.length, connected: allItems.filter(isItemConnected).length, plugins: allItems.filter((i) => itemCategory(i) === "plugins").length, "knowledge-bases": allItems.filter( (i) => itemCategory(i) === "knowledge-bases", ).length, "apps-extensions": allItems.filter( (i) => itemCategory(i) === "apps-extensions", ).length, "ai-clients": allItems.filter((i) => itemCategory(i) === "ai-clients") .length, }), [allItems, isItemConnected], ) useEffect(() => { if (category !== "all" && counts[category] === 0) { void setCategory("all") } }, [category, counts, setCategory]) const railEntries = useMemo(() => { const getSpaceName = (tag?: string): string | null => { if (!tag) return null if (tag === DEFAULT_PROJECT_ID) return "Default" return allProjects.find((p) => p.containerTag === tag)?.name ?? null } const rows: Array<{ ts: number; entry: RailEntry }> = [] for (const [pluginId, key] of activePluginById) { const plugin = PLUGIN_CATALOG[pluginId] if (!plugin) continue const count = activeCountByPlugin.get(pluginId) ?? 1 rows.push({ ts: toMs(key.lastRequest ?? key.createdAt), entry: { kind: "plugin", id: `plugin-${pluginId}`, name: plugin.name, icon: ( {plugin.name} ), pro: !FREE_TIER_PLUGIN_IDS.includes(pluginId), agentCount: count, createdAt: key.createdAt ?? null, lastActive: key.lastRequest ?? null, onManage: () => setConnectedPluginId(pluginId), }, }) } if (activeMcpKey) { rows.push({ ts: toMs(activeMcpKey.lastRequest ?? activeMcpKey.createdAt), entry: { kind: "mcp", id: "mcp", name: "Supermemory MCP", icon: , connectionCount: activeMcpKeys.length, createdAt: activeMcpKey.createdAt ?? null, lastActive: activeMcpKey.lastRequest ?? null, onManage: () => { void setMcpClient("mcp-url") setMcpModalOpen(true) }, }, }) } for (const provider of [ "google-drive", "notion", "onedrive", ] as ConnectorProvider[]) { const conns = connectionsByProvider[provider] const primary = conns[0] if (!primary) continue const meta = CONNECTOR_META[provider] const earliest = conns.reduce((min, c) => { if (!min) return c.createdAt return c.createdAt < min ? c.createdAt : min }, null) const email = conns.find((c) => c.email)?.email ?? null rows.push({ ts: toMs(earliest), entry: { kind: "connector", id: `connector-${provider}`, name: meta.name, documentLabel: meta.documentLabel, icon: meta.icon, pro: true, provider, connection: primary, connectionCount: conns.length, email, spaceName: getSpaceName(primary.containerTags?.[0]), createdAt: earliest, onManage: () => void setAddDoc("connect"), onReconnect: () => addConnectionMutation.mutate(provider), }, }) } rows.sort((a, b) => b.ts - a.ts) return rows.map((r) => r.entry) }, [ activeMcpKey, activeMcpKeys.length, activePluginById, activeCountByPlugin, connectionsByProvider, allProjects, setAddDoc, setMcpClient, addConnectionMutation, ]) const hasActiveRail = railEntries.length > 0 const { data: recentDocs = [], isLoading: recentsLoading } = useQuery({ queryKey: ["integrations-recent-docs", org?.id], queryFn: async () => { const response = await $fetch("@post/documents/documents", { body: { page: 1, limit: 6, sort: "createdAt", order: "desc", containerTags: [], }, disableValidation: true, }) if (response.error) { throw new Error( response.error?.message || "Failed to load recent documents", ) } const data = response.data as z.infer< typeof DocumentsWithMemoriesResponseSchema > return data.documents ?? [] }, enabled: !publicMode && !!org?.id, staleTime: 60 * 1000, }) const railLoading = !publicMode && (apiKeysLoading || connectionsLoading || isAutumnLoading) const showRightColumn = !publicMode && (hasActiveRail || recentDocs.length > 0 || railLoading || recentsLoading) const openRecentDoc = useCallback( (doc: RecentDoc) => { if (onOpenDocument) { onOpenDocument(doc) return } void setDocId(doc.id ?? doc.customId ?? null) }, [onOpenDocument, setDocId], ) const claudeCodeConnected = activePluginById.has("claude_code") const claudeCodeNeedsPro = !isAutumnLoading && !hasProProduct && !isFreeTierPlugin("claude_code") const mcpConnected = !!activeMcpKey const featuredPicks: FeaturedPick[] = [ { id: "feat-poke", name: "Poke", emoji: "🌴", headline: "Your memory, one text away.", support: "recall and save anything by texting Poke", tagline: "Connect Poke to recall and save memories over text.", icon: ( Poke ), backdrop: ( ), ctaLabel: "Connect", onCta: () => { if (publicMode) { redirectToLogin() return } window.open(POKE_RECIPE_URL, "_blank", "noopener,noreferrer") }, }, { id: "feat-mcp", name: "Supermemory MCP", headline: "Your AI tools forget everything between chats.", support: "one setup gives Cursor, Claude & ChatGPT your memory", tagline: "Plug your memory into any MCP client.", icon: , backdrop: ( ), docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction", ctaLabel: mcpConnected ? "Connected" : "Connect", onCta: () => { if (publicMode) { redirectToLogin() return } void setMcpClient(null) setViewMode("mcp") }, }, { id: "feat-claude-code", name: "Claude Code plugin", headline: "Stop re-explaining your codebase every session.", support: "remembers your conventions, decisions & project context", tagline: "Long-term memory for your Claude Code sessions.", icon: ( Claude Code ), backdrop: ( ), docsUrl: "https://supermemory.ai/docs/integrations/claude-code", ctaLabel: publicMode ? "Connect" : claudeCodeConnected ? "Active" : claudeCodeNeedsPro ? "Upgrade" : "Connect", onCta: () => { if (publicMode) { redirectToLogin() return } if (claudeCodeConnected) return if (claudeCodeNeedsPro) { handleUpgrade("api_pro") return } openPluginSetup("claude_code") }, }, { id: "feat-chrome", name: "Chrome Extension", headline: "That article you'll “read later”? Gone by next week.", support: "save anything on the web in one click", tagline: "Save anything on the web, straight from your browser.", icon: , backdrop: , ctaLabel: "Connect", onCta: () => { if (publicMode) { redirectToLogin() return } window.open(CHROME_EXTENSION_URL, "_blank", "noopener,noreferrer") analytics.onboardingChromeExtensionClicked({ source: "integrations" }) }, }, ] const q = search.trim().toLowerCase() const visibleItems = allItems.filter((item) => { if (category === "connected" && !isItemConnected(item)) return false if ( category !== "all" && category !== "connected" && itemCategory(item) !== category ) return false if (q) { const hay = `${item.name} ${item.tagline}`.toLowerCase() if (!hay.includes(q)) return false } return true }) const trackCard = (item: Item) => analytics.integrationCardClicked({ kind: item.kind, id: item.id, name: item.name, }) const renderRight = (item: Item): ReactNode => { if (publicMode) { return ( { trackCard(item) redirectToLogin() }} > Connect ) } switch (item.kind) { case "plugin": { const activeKey = activePluginById.get(item.pluginId) const needsProUpgrade = !isAutumnLoading && !hasProProduct && !isFreeTierPlugin(item.pluginId) if (activeKey) { const busy = connectingPlugin === item.pluginId return ( ) } if (setupPluginIds.has(item.pluginId)) { return ( { trackCard(item) openPluginSetup(item.pluginId) }} /> ) } if (needsProUpgrade) { return ( handleUpgrade("api_pro")}> Upgrade ) } const busy = connectingPlugin === item.pluginId return ( { trackCard(item) openPluginSetup(item.pluginId) }} disabled={!!connectingPlugin} > {busy ? ( <> Connecting… ) : ( "Connect" )} ) } case "connector": { const count = connectionsByProvider[item.provider].length const isGranola = item.provider === "granola" const needsPlanUpgrade = !isAutumnLoading && !connectorAccess if (count > 0) { return (
) } if (needsPlanUpgrade) { return ( handleUpgrade("api_pro")}> Upgrade ) } const busy = connectingProvider === item.provider return ( { trackCard(item) if (isGranola) { if (!connectorAccess) { handleUpgrade("api_pro") return } setGranolaModalOpen(true) return } addConnectionMutation.mutate(item.provider) }} disabled={!!connectingProvider} > {busy ? ( <> Connecting… ) : ( "Connect" )} ) } case "client": { if (item.action.type === "external") { return ( { trackCard(item) window.open( (item.action as { type: "external"; href: string }).href, "_blank", "noopener,noreferrer", ) if (item.id === "chrome") { analytics.onboardingChromeExtensionClicked({ source: "integrations", }) } }} > Connect ) } if (item.id === "shortcuts") { return } return ( { trackCard(item) setViewMode( (item.action as { type: "view"; viewMode: ViewParamValue }) .viewMode, ) }} > Connect ) } case "mcp-client": return ( ) case "import": return ( { trackCard(item) setViewMode(item.viewMode) }} > Connect ) } } const renderInfoRight = (item: Item): ReactNode => { if (publicMode) return renderRight(item) switch (item.kind) { case "plugin": { const activeKey = activePluginById.get(item.pluginId) const needsProUpgrade = !isAutumnLoading && !hasProProduct && !isFreeTierPlugin(item.pluginId) if (activeKey) { const busy = connectingPlugin === item.pluginId return ( { if (needsProUpgrade) { handleUpgrade("api_pro") return } trackCard(item) openPluginSetup(item.pluginId) }} disabled={!!connectingPlugin} > {busy ? ( <> Connecting… ) : ( "Connect" )} ) } return renderRight(item) } case "connector": { const count = connectionsByProvider[item.provider].length if (count > 0) { return ( { trackCard(item) void setAddDoc("connect") }} > Connect ) } return renderRight(item) } case "client": { if (item.id === "shortcuts") { return } return renderRight(item) } default: return renderRight(item) } } const renderStatus = (item: Item): ReactNode => { if (publicMode) return null switch (item.kind) { case "plugin": { const activeKey = activePluginById.get(item.pluginId) if (!activeKey) return null return ( { trackCard(item) setConnectedPluginId(item.pluginId) }} /> ) } case "connector": { const count = connectionsByProvider[item.provider].length if (count <= 0) return null return } case "import": { if (tweetCount <= 0) return null return } default: return null } } const renderItemCard = (item: Item, layoutClassName?: string) => ( ) const renderLeftIndicator = (_item: Item): ReactNode => { return null } const dialogPlugin = newKey.pluginId ? PLUGIN_CATALOG[newKey.pluginId] : undefined const connectedDialogPlugin = connectedPluginId ? PLUGIN_CATALOG[connectedPluginId] : undefined const connectedDialogKeys = connectedPluginId ? activePlugins.filter((key) => key.pluginId === connectedPluginId) : [] const connectedDialogNeedsPro = !!connectedPluginId && !isAutumnLoading && !hasProProduct && !isFreeTierPlugin(connectedPluginId) const pluginSteps = dialogPlugin?.installSteps ?? [] const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_...")) const skipGeneratedKeyStep = stepsEmbedKey || !!dialogPlugin?.usesOAuth const setupSteps: InstallStep[] = skipGeneratedKeyStep ? pluginSteps : [ { title: "Copy your API key", description: "You won't be able to see it again — store it somewhere safe.", code: newKey.key, copyLabel: "API key", secret: true, }, ...pluginSteps, ] return (
{shortcutsConnect.dialog}
{!q && } {showRightColumn && (
setViewMode("list")} />
)}
void setCategory(v)} counts={counts} compact={searchExpanded || !!search} />
{visibleItems.length === 0 ? (

{q ? `No integrations match “${search}”.` : "Nothing in this category yet."}

) : q || category !== "all" ? (
{visibleItems.map((item) => renderItemCard( item, item.id === "shortcuts" ? "sm:w-max sm:min-w-full" : undefined, ), )}
) : (
{SECTION_ORDER.map((cat) => { const items = visibleItems.filter( (i) => itemCategory(i) === cat, ) if (items.length === 0) return null return ( setPluginCommandsOpen(true)} className={cn( dmSans125ClassName(), "inline-flex items-center gap-1.5 rounded-full text-[10px] font-medium text-[#737373] transition-colors hover:text-[#FAFAFA] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#4BA0FA]/60 sm:text-[11px]", )} > Install plugins with one command ) : null } headerSlot={ cat === "ai-clients" && activeMcpKey ? ( ) : null } > {items.map((item) => (
{renderItemCard(item)}
))}
) })}
)}
{showRightColumn && (
setViewMode("list")} />
)}
{ setNewKey((s) => ({ open, key: open ? s.key : "", pluginId: open ? s.pluginId : null, loading: open ? s.loading : false, })) if (!open) { setPluginSetupTab("agent") void setConnectTarget(null) } }} > Set up {dialogPlugin?.name ?? "your plugin"}
{dialogPlugin && ( {dialogPlugin.name} )}

Set up {dialogPlugin?.name ?? "your plugin"}

{pluginSetupTab === "agent" ? "Copy this prompt into your coding agent." : newKey.loading ? "Generating your key…" : "Follow these steps to finish manually."}

{dialogPlugin?.docsUrl && ( Docs )}
{pluginSetupTab === "agent" && dialogPlugin ? ( ) : newKey.loading ? (
Generating your key…
) : newKey.key ? ( ) : (

We couldn't generate the key for the manual setup.

Try again
)}
{ if (!open) setConnectedPluginId(null) }} > {connectedDialogPlugin?.name ?? "Plugin"} connection
{connectedDialogPlugin && ( {connectedDialogPlugin.name} )}

{connectedDialogPlugin?.name ?? "Plugin"}

Active {activePluginById.get(connectedPluginId ?? "")?.lastRequest && ( ·{" "} {formatRelativeTime( activePluginById.get(connectedPluginId ?? "") ?.lastRequest, )} )}

{connectedDialogPlugin?.docsUrl && ( Docs )}

{connectedDialogKeys.length > 1 ? `${connectedDialogKeys.length} connections` : "Connection"}

{connectedDialogKeys.length > 0 ? (
{connectedDialogKeys.map((key) => (
{key.keyStart ? `${key.keyStart}...` : "API key"} void handleRevokePluginKey(key.keyId)} />
))}
) : (

No active connection was found.

)}

Connect this plugin to another agent to run them in parallel.

{connectedDialogNeedsPro ? ( handleUpgrade("api_pro")}> Upgrade to connect more ) : ( { if (!connectedPluginId) return const pluginId = connectedPluginId setConnectedPluginId(null) openPluginSetup(pluginId) }} disabled={!!connectingPlugin} > {connectingPlugin === connectedPluginId ? ( <> Connecting… ) : ( <> Connect another )} )}
{ if (!open) closeMcpModal() else setMcpModalOpen(true) }} > Set up {activeMcpClient?.name ?? "MCP client"}
{activeMcpClient && activeMcpClient.key !== "mcp-url" ? ( {activeMcpClient.name} ) : ( )}

Set up {activeMcpClient?.name ?? "MCP client"}

Connect supermemory MCP to{" "} {activeMcpClient?.name ?? "your client"}.

{ setGranolaModalOpen(open && connectorAccess) if (!open) void setConnectTarget(null) }} />
) }