From a605909191b803d892e0e59dc92adb899a5a948a Mon Sep 17 00:00:00 2001
From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com>
Date: Mon, 4 May 2026 17:15:10 +0000
Subject: [PATCH 1/2] fix: space selector dropdown (#898)
---
apps/web/components/select-spaces-modal.tsx | 49 ++++---
apps/web/components/space-selector.tsx | 139 +++++++++++---------
apps/web/lib/chat-space-label.ts | 5 +-
apps/web/lib/ingest-auto-space.ts | 38 ++++++
4 files changed, 150 insertions(+), 81 deletions(-)
create mode 100644 apps/web/lib/ingest-auto-space.ts
diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx
index d190052cd..31ae6364e 100644
--- a/apps/web/components/select-spaces-modal.tsx
+++ b/apps/web/components/select-spaces-modal.tsx
@@ -9,6 +9,10 @@ import { XIcon, Search, Check } from "lucide-react"
import { Button } from "@ui/components/button"
import { DEFAULT_PROJECT_ID } from "@lib/constants"
import type { ContainerTagListType } from "@lib/types"
+import {
+ compareSpacesUserFirst,
+ spaceSelectorDisplayName,
+} from "@/lib/ingest-auto-space"
interface SelectSpacesModalProps {
isOpen: boolean
@@ -75,24 +79,26 @@ export function SelectSpacesModal({
name: "My Space",
emoji: "📁",
containerTag: DEFAULT_PROJECT_ID,
- }
+ isExperimental: false,
+ isNova: false,
+ createdAt: "",
+ updatedAt: "",
+ } as ContainerTagListType
- const allSpaces = [
- defaultSpace,
- ...projects.filter((p) => p.containerTag !== DEFAULT_PROJECT_ID),
- ]
+ const rest = projects
+ .filter((p) => p.containerTag !== DEFAULT_PROJECT_ID)
+ .sort(compareSpacesUserFirst)
- let result = allSpaces
- if (searchQuery.trim()) {
- const query = searchQuery.toLowerCase()
- result = allSpaces.filter(
- (p) =>
- p.containerTag.toLowerCase().includes(query) ||
- p.name?.toLowerCase().includes(query),
- )
+ const allSpaces = [defaultSpace, ...rest]
+ if (!searchQuery.trim()) {
+ return allSpaces
}
-
- return result
+ const query = searchQuery.trim().toLowerCase()
+ return allSpaces.filter(
+ (p) =>
+ p.containerTag.toLowerCase().includes(query) ||
+ (p.name ?? "").toLowerCase().includes(query),
+ )
}, [projects, searchQuery])
return (
@@ -169,7 +175,7 @@ export function SelectSpacesModal({
type="button"
onClick={() => handleToggle(project.containerTag)}
className={cn(
- "flex items-center gap-3 w-full px-3 py-2.5 rounded-[12px] cursor-pointer transition-colors text-left",
+ "flex min-w-0 max-w-full items-center gap-3 w-full px-3 py-2.5 rounded-[12px] cursor-pointer transition-colors text-left",
isSelected
? "bg-[#14161A] border border-[rgba(82,89,102,0.3)]"
: "bg-transparent border border-transparent hover:bg-[#14161A]/50",
@@ -198,9 +204,14 @@ export function SelectSpacesModal({
{isSelected && }
)}
- {project.emoji || "📁"}
-
- {project.name ?? project.containerTag}
+
+ {project.emoji || "📁"}
+
+
+ {spaceSelectorDisplayName(project, project.containerTag)}
)
diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx
index e8e2e68ce..248889b73 100644
--- a/apps/web/components/space-selector.tsx
+++ b/apps/web/components/space-selector.tsx
@@ -34,6 +34,10 @@ import {
} from "@repo/ui/components/select"
import { Button } from "@repo/ui/components/button"
import { analytics } from "@/lib/analytics"
+import {
+ compareSpacesUserFirst,
+ spaceSelectorDisplayName,
+} from "@/lib/ingest-auto-space"
export interface SpaceSelectorProps {
selectedProjects: string[]
@@ -89,9 +93,19 @@ export function SpaceSelector({
const { allProjects, isLoading } = useContainerTags()
+ const sortedOtherSpaces = useMemo(
+ () =>
+ allProjects
+ .filter(
+ (p: ContainerTagListType) => p.containerTag !== DEFAULT_PROJECT_ID,
+ )
+ .sort(compareSpacesUserFirst),
+ [allProjects],
+ )
+
const displayInfo = useMemo(() => {
if (selectedProjects.length === 1) {
- const containerTag = selectedProjects[0]
+ const containerTag = selectedProjects[0] ?? ""
if (containerTag === DEFAULT_PROJECT_ID) {
return { name: "My Space", emoji: "📁", isMultiple: false }
}
@@ -99,7 +113,7 @@ export function SpaceSelector({
(p: ContainerTagListType) => p.containerTag === containerTag,
)
return {
- name: found?.name || containerTag,
+ name: spaceSelectorDisplayName(found, containerTag),
emoji: found?.emoji || "📁",
isMultiple: false,
}
@@ -113,7 +127,6 @@ export function SpaceSelector({
}
}
- // Nothing selected — default to "My Space"
return { name: "My Space", emoji: "📁", isMultiple: false }
}, [allProjects, selectedProjects])
@@ -261,6 +274,7 @@ export function SpaceSelector({
"min-w-0 truncate text-sm font-medium text-white",
"max-w-[10rem] md:max-w-[15rem]",
)}
+ title={isLoading ? undefined : displayInfo.name}
>
{isLoading ? "…" : displayInfo.name}
@@ -285,7 +299,7 @@ export function SpaceSelector({
-
-
-
-
- My Spaces
-
-
+
+
+
+ My Spaces
+
+
+
handleSelectSingleSpace(DEFAULT_PROJECT_ID)}
className={cn(
- "flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium",
+ "flex min-w-0 max-w-full items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium",
selectedProjects.length === 1 &&
selectedProjects[0] === DEFAULT_PROJECT_ID
? "bg-[#293952]/40"
@@ -312,59 +331,57 @@ export function SpaceSelector({
)}
>
📁
- My Space
+ My Space
- {allProjects
- .filter(
- (p: ContainerTagListType) =>
- p.containerTag !== DEFAULT_PROJECT_ID,
- )
- .map((project: ContainerTagListType) => (
-
- handleSelectSingleSpace(project.containerTag)
- }
- className={cn(
- "flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium group",
- selectedProjects.length === 1 &&
- selectedProjects[0] === project.containerTag
- ? "bg-[#293952]/40"
- : "opacity-60 hover:opacity-100 hover:bg-[#293952]/40",
- )}
+ {sortedOtherSpaces.map((project: ContainerTagListType) => (
+ handleSelectSingleSpace(project.containerTag)}
+ className={cn(
+ "flex min-w-0 max-w-full items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium group",
+ selectedProjects.length === 1 &&
+ selectedProjects[0] === project.containerTag
+ ? "bg-[#293952]/40"
+ : "opacity-60 hover:opacity-100 hover:bg-[#293952]/40",
+ )}
+ >
+
+ {project.emoji || "📁"}
+
+
-
- {project.emoji || "📁"}
-
-
- {project.name ?? project.containerTag}
-
- {enableDelete && (
-
- )}
-
- ))}
+ {spaceSelectorDisplayName(project, project.containerTag)}
+
+ {enableDelete && (
+
+ )}
+
+ ))}
-
+
+ ) : viewMode === "mcp" ? (
+
void setViewMode("integrations")}
+ />
+ ) : viewMode === "plugins" ? (
+ void setViewMode("integrations")}
+ >
+
+
+ ) : viewMode === "chrome" ? (
+ void setViewMode("integrations")}
+ >
+
+
+ ) : viewMode === "shortcuts" ? (
+ void setViewMode("integrations")}
+ >
+
+
+ ) : viewMode === "raycast" ? (
+ void setViewMode("integrations")}
+ >
+
+
+ ) : viewMode === "connections" ? (
+ void setViewMode("integrations")}
+ >
+
+
+ ) : viewMode === "import" ? (
+ void setViewMode("integrations")}
+ />
) : viewMode === "graph" && !isMobile ? (
diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx
index 5465c38b4..544002e0d 100644
--- a/apps/web/app/auth/connect/page.tsx
+++ b/apps/web/app/auth/connect/page.tsx
@@ -349,7 +349,7 @@ function AuthConnectContent() {
{
+ if (isMcpPublicPage) return
if (isRestoring) return
if (!session) {
router.replace(
@@ -21,7 +25,7 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) {
if (organizations.length > 0) return
if (pathname.startsWith("/onboarding")) return
router.replace("/onboarding")
- }, [session, organizations, isRestoring, pathname, router])
+ }, [session, organizations, isRestoring, pathname, router, isMcpPublicPage])
return children
}
diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx
index 2e45cac1f..9dbf66f74 100644
--- a/apps/web/components/header.tsx
+++ b/apps/web/components/header.tsx
@@ -187,11 +187,37 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
key={mode}
type="button"
role="tab"
- aria-selected={viewMode === mode}
+ aria-selected={
+ mode === "integrations"
+ ? [
+ "integrations",
+ "mcp",
+ "plugins",
+ "chrome",
+ "connections",
+ "shortcuts",
+ "raycast",
+ "import",
+ ].includes(viewMode)
+ : viewMode === mode
+ }
onClick={() => void setViewMode(mode)}
className={cn(
"inline-flex h-[calc(100%-1px)] min-h-0 cursor-pointer items-center justify-center gap-1 rounded-full border border-transparent px-2.5 text-xs font-medium whitespace-nowrap transition-colors sm:gap-1.5 sm:px-3 sm:text-sm",
- viewMode === mode
+ (
+ mode === "integrations"
+ ? [
+ "integrations",
+ "mcp",
+ "plugins",
+ "chrome",
+ "connections",
+ "shortcuts",
+ "raycast",
+ "import",
+ ].includes(viewMode)
+ : viewMode === mode
+ )
? "border-[#2261CA33] bg-[#00173C] text-white"
: "text-foreground hover:bg-white/5",
dmSansClassName(),
@@ -375,3 +401,56 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
)
}
+
+export function PublicHeader() {
+ return (
+
+
+
+
+
Your AI
+
+ supermemory
+
+
+
+
+
+
+ Connect your tools, search everything.
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx
index 4ccc45602..55e1215a1 100644
--- a/apps/web/components/integrations-view.tsx
+++ b/apps/web/components/integrations-view.tsx
@@ -1,17 +1,16 @@
"use client"
-import { useState, useEffect } from "react"
-import { useQueryState } from "nuqs"
+import { useQuery } from "@tanstack/react-query"
+import { useCustomer } from "autumn-js/react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
+import { hasActivePlan } from "@lib/queries"
+import { $fetch } from "@lib/api"
+import { authClient } from "@lib/auth"
+import { useAuth } from "@lib/auth-context"
+import type { ConnectionResponseSchema } from "@repo/validation/api"
+import type { z } from "zod"
import { Button } from "@ui/components/button"
-import { MCPDetailView } from "@/components/mcp-modal/mcp-detail-view"
-import { XBookmarksDetailView } from "@/components/onboarding/x-bookmarks-detail-view"
-import { ChromeDetail } from "@/components/integrations/chrome-detail"
-import { ShortcutsDetail } from "@/components/integrations/shortcuts-detail"
-import { RaycastDetail } from "@/components/integrations/raycast-detail"
-import { ConnectionsDetail } from "@/components/integrations/connections-detail"
-import { PluginsDetail } from "@/components/integrations/plugins-detail"
import {
ChromeIcon,
AppleShortcutsIcon,
@@ -19,13 +18,14 @@ import {
} from "@/components/integration-icons"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { ArrowLeft, Sun } from "lucide-react"
-import {
- integrationParam,
- pluginsPanelParam,
- type IntegrationParamValue,
-} from "@/lib/search-params"
+import { CHROME_EXTENSION_URL } from "@repo/lib/constants"
+import { analytics } from "@/lib/analytics"
import Image from "next/image"
import { IntegrationGridCard } from "@/components/integrations/integration-grid-card"
+import { useViewMode } from "@/lib/view-mode-context"
+import type { ViewParamValue } from "@/lib/search-params"
+
+type Connection = z.infer
type CardId =
| "mcp"
@@ -42,6 +42,7 @@ interface IntegrationCardDef {
description: string
icon: React.ReactNode
pro?: boolean
+ externalHref?: string
}
const cards: IntegrationCardDef[] = [
@@ -109,6 +110,7 @@ const cards: IntegrationCardDef[] = [
title: "Chrome Extension",
description: "Save any webpage, import bookmarks, sync ChatGPT memories",
icon: ,
+ externalHref: CHROME_EXTENSION_URL,
},
{
id: "shortcuts",
@@ -130,7 +132,7 @@ const cards: IntegrationCardDef[] = [
},
]
-function DetailWrapper({
+export function DetailWrapper({
onBack,
children,
}: {
@@ -154,74 +156,91 @@ function DetailWrapper({
)
}
-const INTEGRATION_TO_CARD: Record = {
- import: "import",
- chrome: "chrome",
- connections: "connections",
-}
+const CARD_GROUPS: Array<{ label: string; ids: CardId[] }> = [
+ { label: "AI tools", ids: ["plugins", "mcp"] },
+ {
+ label: "Apps & extensions",
+ ids: ["connections", "chrome", "shortcuts", "raycast", "import"],
+ },
+]
export function IntegrationsView() {
- const [integration, setIntegration] = useQueryState(
- "integration",
- integrationParam,
- )
- const [pluginsPanel, setPluginsPanel] = useQueryState(
- "plugins",
- pluginsPanelParam,
- )
- const [selectedCard, setSelectedCard] = useState(null)
+ const { setViewMode } = useViewMode()
+ const { org } = useAuth()
+ const autumn = useCustomer()
+ const hasProProduct = hasActivePlan(autumn.customer?.products, "api_pro")
- useEffect(() => {
- if (pluginsPanel === true) {
- setSelectedCard("plugins")
- return
- }
- if (integration && INTEGRATION_TO_CARD[integration]) {
- setSelectedCard(INTEGRATION_TO_CARD[integration])
- }
- }, [integration, pluginsPanel])
+ const { data: connections = [] } = 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: hasProProduct,
+ })
- const handleBack = () => {
- setSelectedCard(null)
- setIntegration(null)
- void setPluginsPanel(null)
- }
+ const { data: facetsData } = useQuery({
+ queryKey: ["document-facets", []],
+ queryFn: async () => {
+ const response = await $fetch("@post/documents/documents/facets", {
+ body: { containerTags: [] },
+ disableValidation: true,
+ })
+ if (response.error)
+ throw new Error(response.error?.message || "Failed to fetch facets")
+ return response.data as {
+ facets: Array<{ category: string; count: number }>
+ total: number
+ }
+ },
+ staleTime: 5 * 60 * 1000,
+ })
+
+ type ApiKey = { metadata: Record | null }
+ const { data: apiKeys = [] } = useQuery({
+ queryKey: ["api-keys", org?.id],
+ queryFn: async () => {
+ if (!org?.id) return []
+ const data = (await authClient.apiKey.list({
+ fetchOptions: { query: { metadata: { organizationId: org.id } } },
+ })) as unknown as ApiKey[]
+ return data.filter((key) => key.metadata?.organizationId === org.id)
+ },
+ enabled: !!org?.id,
+ staleTime: 30 * 1000,
+ })
+
+ const connectedPluginCount = apiKeys.filter(
+ (key) => key.metadata?.sm_type === "plugin_auth",
+ ).length
- switch (selectedCard) {
- case "mcp":
- return
- case "import":
- return
- case "chrome":
- return (
-
-
-
- )
- case "shortcuts":
- return (
-
-
-
- )
- case "raycast":
- return (
-
-
-
- )
- case "connections":
- return (
-
-
-
- )
- case "plugins":
- return (
-
-
-
- )
+ const tweetCount =
+ facetsData?.facets.find((f) => f.category === "tweet")?.count ?? 0
+
+ const getStatusLabel = (
+ id: CardId,
+ ): { label: string; variant: "connected" | "neutral" } | undefined => {
+ if (id === "connections" && hasProProduct) {
+ return connections.length > 0
+ ? { label: `${connections.length} connected`, variant: "connected" }
+ : { label: "Not connected", variant: "neutral" }
+ }
+ if (id === "import") {
+ return tweetCount > 0
+ ? { label: `${tweetCount} tweets imported`, variant: "connected" }
+ : undefined
+ }
+ if (id === "plugins") {
+ return connectedPluginCount > 0
+ ? { label: `${connectedPluginCount} connected`, variant: "connected" }
+ : undefined
+ }
+ return undefined
}
return (
@@ -237,17 +256,51 @@ export function IntegrationsView() {
-
- {cards.map((card) => (
-
setSelectedCard(card.id)}
- />
- ))}
+
+ {CARD_GROUPS.map((group) => {
+ const groupCards = cards.filter((c) => group.ids.includes(c.id))
+ return (
+
+
+
+ {groupCards.map((card) => {
+ const status = getStatusLabel(card.id)
+ return (
+ {
+ if (card.externalHref) {
+ window.open(
+ card.externalHref,
+ "_blank",
+ "noopener,noreferrer",
+ )
+ analytics.onboardingChromeExtensionClicked({
+ source: "integrations",
+ })
+ } else {
+ void setViewMode(card.id as ViewParamValue)
+ }
+ }}
+ />
+ )
+ })}
+
+
+ )
+ })}
diff --git a/apps/web/components/integrations/connections-detail.tsx b/apps/web/components/integrations/connections-detail.tsx
index d3613400f..5ae871781 100644
--- a/apps/web/components/integrations/connections-detail.tsx
+++ b/apps/web/components/integrations/connections-detail.tsx
@@ -7,7 +7,7 @@ import { hasActivePlan } from "@lib/queries"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
-import { Check, Plus, Trash2, Zap } from "lucide-react"
+import { Check, Clock, FolderOpen, Plus, Trash2, Zap } from "lucide-react"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import { useQueryState } from "nuqs"
@@ -68,10 +68,10 @@ function ConnectionRow({
}
const getProjectName = (tag: string): string => {
- if (tag === DEFAULT_PROJECT_ID) return "Default Project"
+ if (tag === DEFAULT_PROJECT_ID) return "Default"
return (
projects.find((p) => p.containerTag === tag)?.name ??
- tag.replace(/^sm_project_/, "")
+ tag.replace(/^sm_project_/, "").replace(/_/g, " ")
)
}
@@ -136,31 +136,48 @@ function ConnectionRow({
-
- {projectName && (
- <>
+
+
+ {projectName && (
+
+
+
+ {projectName}
+
+
+ )}
+
+
- Project: {projectName}
+ {formatRelativeTime(connection.createdAt)}
-
- >
- )}
-
- Added: {formatRelativeTime(connection.createdAt)}
-
-
-
- {documentCount} {config.documentLabel} connected
-
+
+
+
+
+ {documentCount}
+
+
+ {config.documentLabel}
+
+
diff --git a/apps/web/components/integrations/integration-grid-card.tsx b/apps/web/components/integrations/integration-grid-card.tsx
index cfaf7da0f..5bf6ae917 100644
--- a/apps/web/components/integrations/integration-grid-card.tsx
+++ b/apps/web/components/integrations/integration-grid-card.tsx
@@ -3,18 +3,25 @@
import type { ReactNode } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
+import { ExternalLink } from "lucide-react"
export function IntegrationGridCard({
title,
description,
icon,
pro,
+ statusLabel,
+ statusVariant = "neutral",
+ isExternal,
onClick,
}: {
title: string
description: string
icon: ReactNode
pro?: boolean
+ statusLabel?: string
+ statusVariant?: "connected" | "neutral"
+ isExternal?: boolean
onClick: () => void
}) {
return (
@@ -35,6 +42,9 @@ export function IntegrationGridCard({
PRO
) : null}
+ {isExternal ? (
+
+ ) : null}
{icon}
@@ -48,6 +58,18 @@ export function IntegrationGridCard({
>
{description}
+ {statusLabel ? (
+
+ {statusLabel}
+
+ ) : null}
)
diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx
index 0cbd50e57..8f157a69d 100644
--- a/apps/web/components/integrations/plugins-detail.tsx
+++ b/apps/web/components/integrations/plugins-detail.tsx
@@ -10,12 +10,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
ArrowRight,
BookOpen,
- Brain,
Check,
CheckCircle,
Copy,
ExternalLink,
- Key,
Loader,
Trash2,
Zap,
@@ -30,7 +28,6 @@ import {
DialogTitle,
DialogPortal,
} from "@ui/components/dialog"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/components/tabs"
/** Match `FREE_TIER_PLUGIN_IDS` in mono `packages/lib/plugins.ts`. */
function isFreeTierPlugin(pluginId: string): boolean {
@@ -52,11 +49,11 @@ const PLUGIN_CATALOG: Record = {
id: "claude_code",
name: "Claude Code",
description:
- "Persistent memory for Claude Code. Remembers your coding context, patterns, and decisions across sessions.",
+ "Claude Code remembers your conventions, past decisions, and project context across every session — no re-explaining yourself.",
features: [
- "Auto-recalls relevant context at session start",
- "Captures important observations from tool usage",
- "Builds persistent user profile from interactions",
+ "Picks up where you left off at session start",
+ "Captures decisions and patterns from tool usage",
+ "Builds a persistent profile of how you work",
],
icon: "/images/plugins/claude-code.svg",
docsUrl: "https://docs.supermemory.ai/integrations/claude-code",
@@ -66,11 +63,11 @@ const PLUGIN_CATALOG: Record = {
id: "opencode",
name: "OpenCode",
description:
- "Memory layer for OpenCode. Enhances your coding assistant with long-term memory capabilities.",
+ "Gives OpenCode persistent memory — your patterns, preferences, and decisions carry forward automatically, session to session.",
features: [
"Semantic search across previous sessions",
"Auto-capture of coding decisions",
- "Context injection before each prompt",
+ "Context injected before each prompt",
],
icon: "/images/plugins/opencode.svg",
docsUrl: "https://docs.supermemory.ai/integrations/opencode",
@@ -79,11 +76,11 @@ const PLUGIN_CATALOG: Record = {
id: "openclaw",
name: "OpenClaw",
description:
- "Multi-platform memory for OpenClaw. Works across Telegram, WhatsApp, Discord, Slack and more.",
+ "Persists memory across Telegram, WhatsApp, Discord, and Slack. OpenClaw knows who users are and what they talked about before.",
features: [
- "Cross-channel memory persistence",
+ "Cross-channel memory that follows the user",
"Automatic conversation capture",
- "User profile building across platforms",
+ "User profiles built across every platform",
],
icon: "/images/plugins/openclaw.svg",
docsUrl: "https://docs.supermemory.ai/integrations/openclaw",
@@ -92,11 +89,12 @@ const PLUGIN_CATALOG: Record = {
hermes: {
id: "hermes",
name: "Hermes",
- description: "Memory layer for Hermes agent",
+ description:
+ "Hermes never forgets. Conversations, user profiles, and context persist so every session feels like a continuation, not a cold start.",
features: [
"Semantic search across previous sessions",
"Auto-capture of conversation context",
- "Builds persistent user profile from interactions",
+ "Persistent user profile built over time",
],
icon: "/images/plugins/hermes.svg",
docsUrl: "https://docs.supermemory.ai/integrations/hermes",
@@ -113,119 +111,27 @@ interface ConnectedPlugin {
keyStart?: string | null
}
-function ProUpgradeBanner({ onUpgrade }: { onUpgrade: () => void }) {
+function ProUpgradeNudge({ onUpgrade }: { onUpgrade: () => void }) {
return (
-
-
-
-
-
-
-
-
- Unlock Pro plugins
-
-
- Connect Claude Code, OpenCode, OpenClaw, Cursor, and more with a
- Pro plan.
-
-
-
-
-
- {[
- {
- icon: Brain,
- title: "Context Retention",
- desc: "AI remembers your preferences across sessions",
- },
- {
- icon: Zap,
- title: "Instant Recall",
- desc: "Past decisions surface automatically when relevant",
- },
- {
- icon: Key,
- title: "Secure & Private",
- desc: "Your data stays yours with encrypted storage",
- },
- ].map(({ icon: Icon, title, desc }) => (
-
-
-
-
- {title}
-
-
- {desc}
-
-
-
- ))}
-
-
-
- {Object.values(PLUGIN_CATALOG)
- .filter((p) => !isFreeTierPlugin(p.id))
- .map((plugin) => (
-
-
-
- ))}
-
- Claude Code, OpenCode, OpenClaw & more
-
-
-
-
+
+
+
+
+ Unlock Claude Code, OpenCode, OpenClaw and more with{" "}
+ Pro
+
+
)
}
@@ -321,11 +227,20 @@ function PluginCard({
-
+
{plugin.name}
@@ -349,11 +265,17 @@ function PluginCard({
Connected
)}
+ {needsProUpgrade && (
+
+ PRO
+
+ )}
{plugin.description}
@@ -361,7 +283,7 @@ function PluginCard({
-
+
{plugin.features.map((feature) => (
-
@@ -524,16 +446,6 @@ export function PluginsDetail() {
[connectedPlugins],
)
- const freeConnected = useMemo(
- () => connectedPlugins.filter((p) => isFreeTierPlugin(p.pluginId)),
- [connectedPlugins],
- )
-
- const proConnected = useMemo(
- () => connectedPlugins.filter((p) => !isFreeTierPlugin(p.pluginId)),
- [connectedPlugins],
- )
-
const createPluginKeyMutation = useMutation({
mutationFn: async (pluginId: string) => {
const API_URL =
@@ -607,31 +519,11 @@ export function PluginsDetail() {
const isLoading = autumn.isLoading
const availablePlugins = pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG)
- const freePluginIds = useMemo(() => {
- const ids = new Set(
- availablePlugins.filter(
- (id) => PLUGIN_CATALOG[id] && isFreeTierPlugin(id),
- ),
- )
- if (PLUGIN_CATALOG.hermes) ids.add("hermes")
- return [...ids]
- }, [availablePlugins])
-
- const proPluginIds = useMemo(
- () =>
- availablePlugins.filter(
- (id) => PLUGIN_CATALOG[id] && !isFreeTierPlugin(id),
- ),
- [availablePlugins],
- )
-
const allCatalogPluginIds = useMemo(
() => availablePlugins.filter((id) => PLUGIN_CATALOG[id]),
[availablePlugins],
)
- const showPaidAllInOne = !isLoading && hasProProduct
-
return (
<>
- {showPaidAllInOne ? (
-
- {connectedPlugins.length > 0 && (
-
-
- Connected
-
- {connectedPlugins.map((plugin) => (
-
- ))}
-
- )}
+ {!hasProProduct && !isLoading && (
+
+ )}
-
-
- {connectedPlugins.length > 0
- ? "Add more plugins"
- : "Available plugins"}
-
-
- {allCatalogPluginIds.map((pluginId) => {
- const plugin = PLUGIN_CATALOG[pluginId]
- if (!plugin) return null
- const isConnected = connectedPluginIds.includes(pluginId)
- const isCurrentlyConnecting = connectingPlugin === pluginId
- return (
-
createPluginKeyMutation.mutate(id)}
- onUpgrade={handleUpgrade}
- />
- )
- })}
-
-
-
- ) : (
-
- 0 && (
+
+
-
- Free plugins
-
-
- Pro plugins
-
-
-
-
-
- Included on every plan — connect with no upgrade.
-
-
- {freeConnected.length > 0 && (
-
-
- Connected
-
- {freeConnected.map((plugin) => (
-
- ))}
-
- )}
-
-
-
- {freeConnected.length > 0 ? "Add or manage" : "Available"}
-
-
- {freePluginIds.map((pluginId) => {
- const plugin = PLUGIN_CATALOG[pluginId]
- if (!plugin) return null
- const isConnected = connectedPluginIds.includes(pluginId)
- const isCurrentlyConnecting = connectingPlugin === pluginId
- return (
-
createPluginKeyMutation.mutate(id)}
- onUpgrade={handleUpgrade}
- />
- )
- })}
-
-
-
-
-
- {!hasProProduct && !isLoading && (
-
- )}
-
- {proConnected.length > 0 && (
-
-
- Connected
-
- {proConnected.map((plugin) => (
-
- ))}
-
- )}
-
-
-
- {proConnected.length > 0 ? "Add more" : "Available plugins"}
-
-
- {proPluginIds.map((pluginId) => {
- const plugin = PLUGIN_CATALOG[pluginId]
- if (!plugin) return null
- const isConnected = connectedPluginIds.includes(pluginId)
- const isCurrentlyConnecting = connectingPlugin === pluginId
- const needsProUpgrade = !hasProProduct
- return (
-
createPluginKeyMutation.mutate(id)}
- onUpgrade={handleUpgrade}
- />
- )
- })}
-
-
-
-
+ Connected
+
+ {connectedPlugins.map((plugin) => (
+
+ ))}
+
)}
+
+
+
+ {connectedPlugins.length > 0
+ ? "Add more plugins"
+ : "Available plugins"}
+
+
+ {allCatalogPluginIds.map((pluginId) => {
+ const plugin = PLUGIN_CATALOG[pluginId]
+ if (!plugin) return null
+ const isConnected = connectedPluginIds.includes(pluginId)
+ const isCurrentlyConnecting = connectingPlugin === pluginId
+ const needsProUpgrade =
+ !isLoading && !hasProProduct && !isFreeTierPlugin(pluginId)
+ return (
+
createPluginKeyMutation.mutate(id)}
+ onUpgrade={handleUpgrade}
+ />
+ )
+ })}
+
+