From c7d191e787a2e2b02e6df6850817d1f379bc842e Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Thu, 4 Jun 2026 18:19:06 +0530 Subject: [PATCH 1/4] Refine Codex OAuth and integration status --- apps/web/app/auth/agent-connect/page.tsx | 1 + apps/web/app/auth/connect/page.tsx | 3 +- .../document-cards/plugin-preview.tsx | 8 - .../document-modal/content/plugin-content.tsx | 18 -- apps/web/components/document-modal/index.tsx | 9 +- .../document-modal/plugin-details.tsx | 83 ------ apps/web/components/integrations-view.tsx | 262 +++++++++++++----- .../integrations/plugins-detail.tsx | 191 +++++++++++-- apps/web/lib/plugin-catalog.ts | 2 + 9 files changed, 363 insertions(+), 214 deletions(-) create mode 100644 apps/web/app/auth/agent-connect/page.tsx delete mode 100644 apps/web/components/document-modal/plugin-details.tsx diff --git a/apps/web/app/auth/agent-connect/page.tsx b/apps/web/app/auth/agent-connect/page.tsx new file mode 100644 index 000000000..86838d414 --- /dev/null +++ b/apps/web/app/auth/agent-connect/page.tsx @@ -0,0 +1 @@ +export { default } from "../connect/page" diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index eade1f096..0f96e52c0 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -99,7 +99,7 @@ const PLUGIN_INFO: Record = { "Captures coding decisions and patterns automatically", "Builds persistent user profile across projects", ], - icon: "/images/plugins/codex.svg", + icon: "/images/plugins/codex.png", }, } @@ -199,6 +199,7 @@ function AuthConnectContent() { const redirectUrl = new URL(callback) redirectUrl.searchParams.set("apikey", data.key) + redirectUrl.searchParams.set("api_url", API_URL) window.location.href = redirectUrl.toString() } catch (err) { console.error("Failed to get API key:", err) diff --git a/apps/web/components/document-cards/plugin-preview.tsx b/apps/web/components/document-cards/plugin-preview.tsx index e4b904192..bd40e76c1 100644 --- a/apps/web/components/document-cards/plugin-preview.tsx +++ b/apps/web/components/document-cards/plugin-preview.tsx @@ -28,15 +28,7 @@ export function PluginPreview({ parsed }: { parsed: ParsedPluginDocument }) { )} {parsed.pluginLabel} -

- {parsed.formatLabel} -

- {parsed.identifierValue && ( -

- {parsed.identifierValue} -

- )}

{parsed.pluginLabel} - - {parsed.formatLabel} - - {parsed.identifierLabel && parsed.identifierValue && ( - - {parsed.identifierLabel}: {parsed.identifierValue} - - )}

type DocumentWithMemories = DocumentsResponse["documents"][0] @@ -270,13 +269,8 @@ export function DocumentModal({ ], ) - const hasPluginInsights = - pluginDocument && - pluginDocument.kind !== "claude-code-doc" && - pluginDocument.kind !== "openclaw-session" const hasDocumentInsights = Boolean( - hasPluginInsights || - _document?.summary || + _document?.summary || pluginDocument?.summary || (_document?.memoryEntries && _document.memoryEntries.length > 0), ) @@ -303,7 +297,6 @@ export function DocumentModal({ dmSansClassName(), )} > - {hasPluginInsights && } {_document && (_document.summary || pluginDocument?.summary) && ( -

- {label} -

-

{value}

- - ) -} - -export function PluginDetails({ parsed }: { parsed: ParsedPluginDocument }) { - return ( -
-
-

- Details -

- - {parsed.pluginIconSrc && ( - - )} - {parsed.pluginLabel} - -
-
- - {parsed.identifierLabel && parsed.identifierValue && ( - - )} - {parsed.clientLabel && parsed.clientValue && ( - - )} -
- {parsed.artifacts.length > 0 && ( -
-

- Outputs -

-
- {parsed.artifacts.map((artifact, index) => ( - - ))} -
-
- )} -
- ) -} diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index 5a5a71618..f24188bf6 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -23,7 +23,6 @@ import { ArrowRight, BookOpen, Check, - ChevronDown, Loader, Search, X, @@ -46,11 +45,11 @@ import { import { AnimatePresence, motion } from "motion/react" import { toast } from "sonner" import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" -import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" import { PLUGIN_CATALOG, FREE_TIER_PLUGIN_IDS, isFreeTierPlugin, + normalizePluginClientId, type InstallStep, } from "@/lib/plugin-catalog" import { INSET, InstallSteps, PillButton } from "./integrations/install-steps" @@ -66,6 +65,16 @@ interface ConnectedKey { pluginId: string } +type ListedApiKey = { + id: string + name?: string | null + createdAt?: string + enabled?: boolean + lastRequest?: string | null + metadata: string | Record | null + start?: string | null +} + type ItemKind = "plugin" | "connector" | "client" | "mcp-client" | "import" type MCPClientKey = @@ -487,56 +496,20 @@ function DisconnectButton({ onConfirm }: { onConfirm: () => void }) { ) } -function ConnectedPill({ - keys, - onRevoke, -}: { - keys: ConnectedKey[] - onRevoke: (keyId: string) => void -}) { +function ConnectedButton({ onClick }: { onClick: () => void }) { return ( - - - - - e.stopPropagation()} - className={cn( - dmSans125ClassName(), - "w-[260px] rounded-xl border border-white/10 bg-[#1B1F24] p-2 text-[#FAFAFA]", - )} - > -

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

-
- {keys.map((k) => ( -
- - {k.keyStart ? `${k.keyStart}…` : "API key"} - - onRevoke(k.keyId)} /> -
- ))} -
-
-
+ ) } @@ -1002,6 +975,9 @@ export function IntegrationsView() { key: string pluginId: string | null }>({ open: false, key: "", pluginId: null }) + const [connectedPluginId, setConnectedPluginId] = useState( + null, + ) const { data: pluginsData } = useQuery({ queryFn: async () => { @@ -1030,27 +1006,34 @@ export function IntegrationsView() { enabled: hasProProduct, }) - type ApiKey = { - id: string - metadata: Record | null - start: string | null - } - const { data: apiKeys = [], refetch: refetchKeys } = 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) + const { data: apiKeys = [], refetch: refetchKeys } = 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: !!org?.id, + staleTime: 30 * 1000, }, - enabled: !!org?.id, - staleTime: 30 * 1000, - }) + ) + + const keyPrefix = useCallback((key: ListedApiKey): string | null => { + return key.start ?? (key.name?.startsWith("sm_") ? key.name : null) + }, []) const connectedPlugins = useMemo(() => { const out: ConnectedKey[] = [] for (const key of apiKeys) { + if (key.enabled === false) continue + if (!key.lastRequest) continue if (!key.metadata) continue try { const metadata = @@ -1063,14 +1046,14 @@ export function IntegrationsView() { if (metadata.sm_type === "plugin_auth" && metadata.sm_client) { out.push({ keyId: key.id, - keyStart: key.start ?? null, - pluginId: metadata.sm_client, + keyStart: keyPrefix(key), + pluginId: normalizePluginClientId(metadata.sm_client), }) } } catch {} } return out - }, [apiKeys]) + }, [apiKeys, keyPrefix]) const connectionsByProvider = useMemo(() => { const out: Record = { @@ -1368,7 +1351,14 @@ export function IntegrationsView() { const needsProUpgrade = !isAutumnLoading && !hasProProduct && !isFreeTierPlugin(item.pluginId) if (keys.length > 0) { - return + return ( + { + trackCard(item) + setConnectedPluginId(item.pluginId) + }} + /> + ) } if (needsProUpgrade) { return ( @@ -1500,12 +1490,7 @@ export function IntegrationsView() { const renderLeftIndicator = (item: Item): ReactNode => { if (item.kind === "plugin") { - const isConnected = connectedPlugins.some( - (k) => k.pluginId === item.pluginId, - ) - return isConnected ? ( - - ) : null + return null } if (item.kind === "connector") { const count = connectionsByProvider[item.provider].length @@ -1519,6 +1504,12 @@ export function IntegrationsView() { const dialogPlugin = newKey.pluginId ? PLUGIN_CATALOG[newKey.pluginId] : undefined + const connectedDialogPlugin = connectedPluginId + ? PLUGIN_CATALOG[connectedPluginId] + : undefined + const connectedDialogKeys = connectedPluginId + ? connectedPlugins.filter((key) => key.pluginId === connectedPluginId) + : [] const pluginSteps = dialogPlugin?.installSteps ?? [] const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_...")) const setupSteps: InstallStep[] = stepsEmbedKey @@ -1699,6 +1690,123 @@ export function IntegrationsView() { + { + if (!open) setConnectedPluginId(null) + }} + > + + + {connectedDialogPlugin?.name ?? "Plugin"} connection + +
+ {connectedDialogPlugin && ( + + {connectedDialogPlugin.name} + + )} +
+

+ {connectedDialogPlugin?.name ?? "Plugin"} connected +

+

+ + Active +

+
+
+ {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. +

+ )} +
+
+ + + +
+
+
+ { diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx index 053e7156b..a77ec214b 100644 --- a/apps/web/components/integrations/plugins-detail.tsx +++ b/apps/web/components/integrations/plugins-detail.tsx @@ -8,7 +8,15 @@ import { hasActivePlan } from "@lib/queries" import { useCustomer } from "autumn-js/react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import * as DialogPrimitive from "@radix-ui/react-dialog" -import { BookOpen, Check, ChevronDown, Loader, X, Zap } from "lucide-react" +import { + BookOpen, + Check, + ChevronDown, + ExternalLink, + Loader, + X, + Zap, +} from "lucide-react" import Image from "next/image" import { type ReactNode, useEffect, useMemo, useState } from "react" import { toast } from "sonner" @@ -17,6 +25,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" import { PLUGIN_CATALOG, isFreeTierPlugin, + normalizePluginClientId, type InstallStep, type PluginInfo, } from "@/lib/plugin-catalog" @@ -27,9 +36,20 @@ interface ConnectedPlugin { keyId: string pluginId: string createdAt: string + lastRequest?: string | null keyStart?: string | null } +type ListedApiKey = { + id: string + name?: string | null + createdAt: string + enabled?: boolean + lastRequest: string | null + metadata: string | Record | null + start?: string | null +} + function SectionHeader({ children }: { children: ReactNode }) { return (

void }) { ) } -function ConnectedPill({ +function toDate(value: string | Date | null | undefined): Date | null { + if (!value) return null + const date = value instanceof Date ? value : new Date(value) + return Number.isNaN(date.getTime()) ? null : date +} + +function formatRelativeTime(value: string | Date | null | undefined): string { + const date = toDate(value) + if (!date) return "Never" + const absMs = Math.abs(Date.now() - date.getTime()) + const minute = 60 * 1000 + const hour = 60 * minute + const day = 24 * hour + if (absMs < minute) return "Just now" + if (absMs < hour) return `${Math.round(absMs / minute)}m ago` + if (absMs < day) return `${Math.round(absMs / hour)}h ago` + return `${Math.round(absMs / day)}d ago` +} + +function formatDate(value: string | Date | null | undefined): string { + const date = toDate(value) + if (!date) return "Unknown" + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) +} + +function maskKey(start: string | null | undefined): string { + if (!start) return "sm_********" + return `${start}********` +} + +function keyPrefix(key: ListedApiKey): string | null { + return key.start ?? (key.name?.startsWith("sm_") ? key.name : null) +} + +function DetailStat({ label, value }: { label: string; value: string }) { + return ( +

+

+ {label} +

+

+ {value} +

+
+ ) +} + +function ActivePill({ + plugin, connectedKeys, onRevoke, }: { + plugin: PluginInfo connectedKeys: ConnectedPlugin[] onRevoke: (keyId: string) => void }) { + const primaryKey = connectedKeys[0] return ( @@ -140,7 +224,7 @@ function ConnectedPill({ )} > - Connected + ACTIVE @@ -148,13 +232,67 @@ function ConnectedPill({ align="end" className={cn( dmSans125ClassName(), - "w-[260px] rounded-xl border border-white/10 bg-[#1B1F24] p-2 text-[#FAFAFA]", + "w-[min(380px,calc(100vw-32px))] rounded-xl border border-white/10 bg-[#1B1F24] p-4 text-[#FAFAFA]", )} > +
+ +
+

+ {plugin.name} +

+

Connected

+
+ +
+ +
+ + + +
+ +
+ {plugin.docsUrl && ( + + Docs + + )} + {plugin.githubUrl && ( + + GitHub + + )} + + Connected {formatDate(primaryKey?.createdAt)} + +
+

{connectedKeys.length > 1 @@ -240,7 +378,11 @@ function PluginRow({

{plugin.docsUrl && } {isConnected ? ( - + ) : needsProUpgrade ? ( Upgrade @@ -332,21 +474,29 @@ export function PluginsDetail() { queryKey: ["plugins"], }) - const { data: apiKeys = [], refetch: refetchKeys } = useQuery({ - enabled: !!org?.id, - queryFn: async () => { - if (!org?.id) return [] - const data = await authClient.apiKey.list({ - fetchOptions: { query: { metadata: { organizationId: org.id } } }, - }) - return data.filter((key) => key.metadata?.organizationId === org.id) + const { data: apiKeys = [], refetch: refetchKeys } = useQuery( + { + enabled: !!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 ?? [] + }, + queryKey: ["api-keys", org?.id], }, - queryKey: ["api-keys", org?.id], - }) + ) const connectedPlugins = useMemo(() => { const plugins: ConnectedPlugin[] = [] for (const key of apiKeys) { + if (key.enabled === false) continue + if (!key.lastRequest) continue if (!key.metadata) continue try { const metadata = @@ -358,12 +508,15 @@ export function PluginsDetail() { : (key.metadata as { sm_type?: string; sm_client?: string }) if (metadata.sm_type === "plugin_auth" && metadata.sm_client) { + const createdAt = toDate(key.createdAt)?.toISOString() + const lastRequest = toDate(key.lastRequest)?.toISOString() plugins.push({ id: key.id, keyId: key.id, - pluginId: metadata.sm_client, - createdAt: key.createdAt.toISOString(), - keyStart: key.start ?? null, + pluginId: normalizePluginClientId(metadata.sm_client), + createdAt: createdAt ?? new Date().toISOString(), + lastRequest: lastRequest ?? null, + keyStart: keyPrefix(key), }) } } catch {} diff --git a/apps/web/lib/plugin-catalog.ts b/apps/web/lib/plugin-catalog.ts index f94a4e929..f16d087ef 100644 --- a/apps/web/lib/plugin-catalog.ts +++ b/apps/web/lib/plugin-catalog.ts @@ -14,6 +14,7 @@ export interface PluginInfo { tagline: string icon: string docsUrl?: string + githubUrl?: string /** Steps shown after a key is minted. The literal `sm_...` is replaced * with the freshly generated key when rendered. */ installSteps?: InstallStep[] @@ -55,6 +56,7 @@ export const PLUGIN_CATALOG: Record = { tagline: "Persistent memory for the Codex CLI — free on every plan", icon: "/images/plugins/codex.png", docsUrl: "https://docs.supermemory.ai/integrations/codex", + githubUrl: "https://github.com/supermemoryai/codex-supermemory", installSteps: [ { title: "Save your API key", From bbfc059f961c81e469d002680f64c4b67cef4a57 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Thu, 4 Jun 2026 18:39:35 +0530 Subject: [PATCH 2/4] Add OpenCode OAuth setup to integrations --- apps/web/components/integrations-view.tsx | 3 ++- .../components/integrations/plugins-detail.tsx | 6 ++++-- apps/web/lib/plugin-catalog.ts | 17 +++++++++-------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index f24188bf6..fb77040fa 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -1512,7 +1512,8 @@ export function IntegrationsView() { : [] const pluginSteps = dialogPlugin?.installSteps ?? [] const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_...")) - const setupSteps: InstallStep[] = stepsEmbedKey + const skipGeneratedKeyStep = stepsEmbedKey || !!dialogPlugin?.usesOAuth + const setupSteps: InstallStep[] = skipGeneratedKeyStep ? pluginSteps : [ { diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx index a77ec214b..e121869cb 100644 --- a/apps/web/components/integrations/plugins-detail.tsx +++ b/apps/web/components/integrations/plugins-detail.tsx @@ -620,9 +620,11 @@ export function PluginsDetail() { const pluginSteps = dialogPlugin?.installSteps ?? [] // If a step already embeds the key (an `export …="sm_…"` line), don't also // show the bare key in its own step — that's the repetition to avoid. - // Otherwise (wizard-style installs) lead with a copy-the-key step. + // Otherwise (wizard-style installs) lead with a copy-the-key step, unless + // the plugin performs browser OAuth itself. const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_...")) - const setupSteps: InstallStep[] = stepsEmbedKey + const skipGeneratedKeyStep = stepsEmbedKey || !!dialogPlugin?.usesOAuth + const setupSteps: InstallStep[] = skipGeneratedKeyStep ? pluginSteps : [ { diff --git a/apps/web/lib/plugin-catalog.ts b/apps/web/lib/plugin-catalog.ts index f16d087ef..9f17d1567 100644 --- a/apps/web/lib/plugin-catalog.ts +++ b/apps/web/lib/plugin-catalog.ts @@ -15,6 +15,7 @@ export interface PluginInfo { icon: string docsUrl?: string githubUrl?: string + usesOAuth?: boolean /** Steps shown after a key is minted. The literal `sm_...` is replaced * with the freshly generated key when rendered. */ installSteps?: InstallStep[] @@ -79,20 +80,20 @@ export const PLUGIN_CATALOG: Record = { tagline: "Long-term memory for your OpenCode sessions", icon: "/images/plugins/opencode.svg", docsUrl: "https://docs.supermemory.ai/integrations/opencode", + githubUrl: "https://github.com/supermemoryai/opencode-supermemory", + usesOAuth: true, installSteps: [ - { - title: "Save your API key", - description: - "Add this to your shell profile. This key is shown only once — save it now.", - code: 'export SUPERMEMORY_API_KEY="sm_..."', - copyLabel: "API key", - secret: true, - }, { title: "Install the plugin", description: "Use --no-tui for non-interactive environments.", code: "bunx opencode-supermemory@latest install", }, + { + title: "Authenticate OpenCode", + description: + "Run the browser auth flow from the machine where OpenCode runs:", + code: "bunx opencode-supermemory@latest login", + }, { title: "Verify your config", description: From d2a2d1e325755d6fe5994dc6dffaa74d5e9616b1 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Thu, 4 Jun 2026 14:34:48 -0700 Subject: [PATCH 3/4] feat: rework plugin/connector integration cards around active state - Show plugins as Active (used) vs Finish setup (key, no use) vs Connect - Active pill becomes subtle ghost status; dedicated + button connects another agent - Support multiple connections per plugin with count + manage modal - Mirror pattern for connectors: subtle connected status + add-knowledge shortcut - Move Pro to subtle accent on name row, Docs alone top-right - Wrap items into a grid for any specific category filter (rails only on All) - Keep featured hero visible on the Active filter --- apps/web/components/integrations-view.tsx | 385 +++++++++++++++--- .../integrations/plugins-detail.tsx | 135 +++++- 2 files changed, 455 insertions(+), 65 deletions(-) diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index fb77040fa..e1d832571 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -24,16 +24,19 @@ import { BookOpen, Check, Loader, + Plus, Search, X, Zap, } from "lucide-react" +import { formatRelativeTime } from "@/components/settings/sync-utils" import { CHROME_EXTENSION_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 } from "@/lib/search-params" import { useCallback, useEffect, @@ -63,14 +66,55 @@ interface ConnectedKey { keyId: string keyStart: string | null pluginId: string + lastRequest?: string | null + createdAt?: string | null +} + +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 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 } } type ListedApiKey = { id: string name?: string | null - createdAt?: string + createdAt?: string | Date | null enabled?: boolean - lastRequest?: string | null + lastRequest?: string | Date | null metadata: string | Record | null start?: string | null } @@ -194,7 +238,7 @@ const catParam = parseAsStringEnum([ const CATEGORY_LABEL: Record = { all: "All", - connected: "Connected", + connected: "Active", plugins: "Plugins", "knowledge-bases": "Knowledge bases", "apps-extensions": "Apps & extensions", @@ -432,7 +476,7 @@ function ProChip() { Pro @@ -496,30 +540,52 @@ function DisconnectButton({ onConfirm }: { onConfirm: () => void }) { ) } -function ConnectedButton({ onClick }: { onClick: () => void }) { +function ActiveButton({ + count, + lastActive, + onClick, +}: { + count: number + lastActive?: string | null + onClick: () => void +}) { return ( ) } +function FinishSetupButton({ onClick }: { onClick: () => void }) { + return ( + + + Finish setup + + ) +} + function ConnectionsCountPill({ count }: { count: number }) { return ( @@ -567,7 +633,7 @@ function ItemCard({
-
+
{leftIndicator} ( null, ) + const [finishSetupPluginId, setFinishSetupPluginId] = useState< + string | null + >(null) const { data: pluginsData } = useQuery({ queryFn: async () => { @@ -1029,31 +1098,40 @@ export function IntegrationsView() { return key.start ?? (key.name?.startsWith("sm_") ? key.name : null) }, []) - const connectedPlugins = useMemo(() => { - const out: ConnectedKey[] = [] - for (const key of apiKeys) { - if (key.enabled === false) continue - if (!key.lastRequest) 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) { - out.push({ - keyId: key.id, - keyStart: keyPrefix(key), - pluginId: normalizePluginClientId(metadata.sm_client), - }) - } - } catch {} + const { active: activePlugins, setup: setupPlugins } = useMemo( + () => parsePluginAuthKeys(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 out - }, [apiKeys, keyPrefix]) + return map + }, [activePlugins]) + + 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 = { @@ -1171,6 +1249,7 @@ export function IntegrationsView() { ) const [category, setCategory] = useQueryState("cat", catParam) + const [, setAddDoc] = useQueryState("add", addDocumentParam) const [mcpClient, setMcpClient] = useQueryState("mcpClient", parseAsString) const [mcpModalOpen, setMcpModalOpen] = useState(false) const [search, setSearch] = useState("") @@ -1201,14 +1280,14 @@ export function IntegrationsView() { const isItemConnected = useCallback( (item: Item): boolean => { if (item.kind === "plugin") { - return connectedPlugins.some((k) => k.pluginId === item.pluginId) + return activePluginById.has(item.pluginId) } if (item.kind === "connector") { return connectionsByProvider[item.provider].length > 0 } return false }, - [connectedPlugins, connectionsByProvider], + [activePluginById, connectionsByProvider], ) const counts = useMemo>( @@ -1234,9 +1313,7 @@ export function IntegrationsView() { } }, [category, counts, setCategory]) - const claudeCodeConnected = connectedPlugins.some( - (k) => k.pluginId === "claude_code", - ) + const claudeCodeConnected = activePluginById.has("claude_code") const claudeCodeNeedsPro = !isAutumnLoading && !hasProProduct && !isFreeTierPlugin("claude_code") @@ -1290,7 +1367,7 @@ export function IntegrationsView() { ), docsUrl: "https://docs.supermemory.ai/integrations/claude-code", ctaLabel: claudeCodeConnected - ? "Connected" + ? "Active" : claudeCodeNeedsPro ? "Upgrade" : "Connect", @@ -1345,17 +1422,55 @@ export function IntegrationsView() { const renderRight = (item: Item): ReactNode => { switch (item.kind) { case "plugin": { - const keys = connectedPlugins.filter( - (k) => k.pluginId === item.pluginId, - ) + const activeKey = activePluginById.get(item.pluginId) + const activeCount = activeCountByPlugin.get(item.pluginId) ?? 0 const needsProUpgrade = !isAutumnLoading && !hasProProduct && !isFreeTierPlugin(item.pluginId) - if (keys.length > 0) { + if (activeKey) { + const busy = connectingPlugin === item.pluginId return ( - + { + trackCard(item) + setConnectedPluginId(item.pluginId) + }} + /> + +
+ ) + } + if (setupPluginIds.has(item.pluginId)) { + return ( + { trackCard(item) - setConnectedPluginId(item.pluginId) + setFinishSetupPluginId(item.pluginId) }} /> ) @@ -1389,7 +1504,28 @@ export function IntegrationsView() { case "connector": { const count = connectionsByProvider[item.provider].length const needsProUpgrade = !isAutumnLoading && !hasProProduct - if (count > 0) return + if (count > 0) { + return ( +
+ + +
+ ) + } if (needsProUpgrade) { return ( @@ -1488,16 +1624,7 @@ export function IntegrationsView() { /> ) - const renderLeftIndicator = (item: Item): ReactNode => { - if (item.kind === "plugin") { - return null - } - if (item.kind === "connector") { - const count = connectionsByProvider[item.provider].length - return count > 0 ? ( - - ) : null - } + const renderLeftIndicator = (_item: Item): ReactNode => { return null } @@ -1508,8 +1635,17 @@ export function IntegrationsView() { ? PLUGIN_CATALOG[connectedPluginId] : undefined const connectedDialogKeys = connectedPluginId - ? connectedPlugins.filter((key) => key.pluginId === connectedPluginId) + ? activePlugins.filter((key) => key.pluginId === connectedPluginId) : [] + const connectedDialogNeedsPro = + !!connectedPluginId && + !isAutumnLoading && + !hasProProduct && + !isFreeTierPlugin(connectedPluginId) + const finishSetupPlugin = finishSetupPluginId + ? PLUGIN_CATALOG[finishSetupPluginId] + : undefined + const finishSetupSteps = finishSetupPlugin?.installSteps ?? [] const pluginSteps = dialogPlugin?.installSteps ?? [] const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_...")) const skipGeneratedKeyStep = stepsEmbedKey || !!dialogPlugin?.usesOAuth @@ -1530,9 +1666,7 @@ export function IntegrationsView() { return (
- {!q && category !== "connected" && ( - - )} + {!q && }
- ) : q ? ( + ) : q || category !== "all" ? (
{visibleItems.map((item) => renderItemCard(item))}
@@ -1724,11 +1858,20 @@ export function IntegrationsView() { )}

- {connectedDialogPlugin?.name ?? "Plugin"} connected + {connectedDialogPlugin?.name ?? "Plugin"}

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

@@ -1790,6 +1933,120 @@ export function IntegrationsView() { No active connection was found.

)} +

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

+
+
+ {connectedDialogNeedsPro ? ( + + Upgrade to connect + more + + ) : ( + { + if (!connectedPluginId) return + const pluginId = connectedPluginId + setConnectedPluginId(null) + createPluginKeyMutation.mutate(pluginId) + }} + disabled={!!connectingPlugin} + > + {connectingPlugin === connectedPluginId ? ( + <> + Connecting… + + ) : ( + <> + Connect another + + )} + + )} + + + +
+ +
+ + { + if (!open) setFinishSetupPluginId(null) + }} + > + + + Finish setup {finishSetupPlugin?.name ?? "plugin"} + +
+ {finishSetupPlugin && ( + + {finishSetupPlugin.name} + + )} +
+

+ Finish setup {finishSetupPlugin?.name ?? "plugin"} +

+

+ Complete install in the tool — this card turns active after the + first API call. +

+
+ + + +
+
+
+ {finishSetupSteps.length > 0 ? ( + + ) : ( +

+ Open {finishSetupPlugin?.name ?? "the plugin"} and finish + authentication, then send a test memory. +

+ )} +
diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx index e121869cb..0bd044ae8 100644 --- a/apps/web/components/integrations/plugins-detail.tsx +++ b/apps/web/components/integrations/plugins-detail.tsx @@ -246,7 +246,7 @@ function ActivePill({ > {plugin.name}

-

Connected

+

Active

@@ -326,22 +326,26 @@ function PluginRow({ plugin, pluginId, connectedKeys, + needsSetup, needsProUpgrade, isConnecting, actionsDisabled, onConnect, onUpgrade, onRevoke, + onFinishSetup, }: { plugin: PluginInfo pluginId: string connectedKeys: ConnectedPlugin[] + needsSetup: boolean needsProUpgrade: boolean isConnecting: boolean actionsDisabled: boolean onConnect: (id: string) => void onUpgrade: () => void onRevoke: (keyId: string) => void + onFinishSetup: (id: string) => void }) { const isConnected = connectedKeys.length > 0 return ( @@ -383,6 +387,11 @@ function PluginRow({ connectedKeys={connectedKeys} onRevoke={onRevoke} /> + ) : needsSetup ? ( + onFinishSetup(pluginId)}> + + Finish setup + ) : needsProUpgrade ? ( Upgrade @@ -449,6 +458,9 @@ export function PluginsDetail() { const queryClient = useQueryClient() const [tierFilter, setTierFilter] = useState("all") const [connectingPlugin, setConnectingPlugin] = useState(null) + const [finishSetupPluginId, setFinishSetupPluginId] = useState( + null, + ) const [newKey, setNewKey] = useState<{ open: boolean key: string @@ -492,6 +504,28 @@ export function PluginsDetail() { }, ) + const setupPluginIds = useMemo(() => { + const ids = new Set() + for (const key of apiKeys) { + if (key.enabled === false) continue + if (key.lastRequest) 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) { + ids.add(normalizePluginClientId(metadata.sm_client)) + } + } catch {} + } + return ids + }, [apiKeys]) + const connectedPlugins = useMemo(() => { const plugins: ConnectedPlugin[] = [] for (const key of apiKeys) { @@ -616,6 +650,9 @@ export function PluginsDetail() { const dialogPlugin = newKey.pluginId ? PLUGIN_CATALOG[newKey.pluginId] : undefined + const finishSetupPlugin = finishSetupPluginId + ? PLUGIN_CATALOG[finishSetupPluginId] + : undefined const pluginSteps = dialogPlugin?.installSteps ?? [] // If a step already embeds the key (an `export …="sm_…"` line), don't also @@ -667,10 +704,15 @@ export function PluginsDetail() { connectedKeys={connectedPlugins.filter( (p) => p.pluginId === pluginId, )} + needsSetup={ + !connectedPluginIds.has(pluginId) && + setupPluginIds.has(pluginId) + } needsProUpgrade={needsProUpgrade} isConnecting={connectingPlugin === pluginId} actionsDisabled={!!connectingPlugin} onConnect={(id) => createPluginKeyMutation.mutate(id)} + onFinishSetup={(id) => setFinishSetupPluginId(id)} onUpgrade={handleUpgrade} onRevoke={handleRevoke} /> @@ -793,6 +835,97 @@ export function PluginsDetail() {
+ + { + if (!open) setFinishSetupPluginId(null) + }} + > + + + Finish setup {finishSetupPlugin?.name ?? "plugin"} + +
+ {finishSetupPlugin && ( + + )} +
+

+ Finish setup {finishSetupPlugin?.name ?? "plugin"} +

+

+ Complete install in the tool — status becomes active after the + first API call. +

+
+ + + +
+
+
+ {finishSetupPlugin?.installSteps?.length ? ( + + ) : ( +

+ Open {finishSetupPlugin?.name ?? "the plugin"} and finish + authentication. +

+ )} +
+
+
+ + + +
+
+
) } From 3d503949e2aa6700be3517a1591fefa9fd63725f Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:38:03 +0000 Subject: [PATCH 4/4] fix: format useState type annotation for Biome CI Co-Authored-By: Claude Opus 4.5 --- apps/web/components/integrations-view.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index e1d832571..a812cce70 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -1044,9 +1044,9 @@ export function IntegrationsView() { const [connectedPluginId, setConnectedPluginId] = useState( null, ) - const [finishSetupPluginId, setFinishSetupPluginId] = useState< - string | null - >(null) + const [finishSetupPluginId, setFinishSetupPluginId] = useState( + null, + ) const { data: pluginsData } = useQuery({ queryFn: async () => {