Add multi-plugin OAuth connect UI (#1163)

Co-authored-by: Ishaan Gupta <ishaankone@gmail.com>
This commit is contained in:
Ishaan Gupta 2026-08-14 15:51:22 +05:30 committed by GitHub
parent 957011d8fe
commit e217dde97e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -7,7 +7,7 @@ import { dmSans125ClassName } from "@/lib/fonts"
import { ArrowRight, XCircle } from "lucide-react"
import Image from "next/image"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useEffect, useState } from "react"
import { Suspense, useEffect, useMemo, useState } from "react"
import { PENDING_CONNECT_URL_KEY } from "@/lib/constants"
@ -87,7 +87,7 @@ const PLUGIN_INFO: Record<string, PluginInfo> = {
"Auto-capture of project decisions",
"Context-aware suggestions",
],
icon: "/images/plugins/cursor.svg",
icon: "/images/plugins/cursor.png",
},
codex: {
name: "OpenAI Codex",
@ -102,10 +102,76 @@ const PLUGIN_INFO: Record<string, PluginInfo> = {
},
}
const MULTI_PLUGIN_FEATURES = [
"Share one persistent memory layer across selected coding agents.",
"Recall project context, coding decisions, and prior sessions.",
"Connect every selected plugin with one approval.",
]
function isKnownPlugin(value: string): boolean {
return Object.hasOwn(PLUGIN_INFO, value)
}
function getPluginName(client: string): string {
return PLUGIN_INFO[client]?.name ?? "External Tool"
}
function formatPluginNames(clients: string[]): string {
const names = clients.map((id) => getPluginName(id))
if (names.length === 0) return "External Tool"
if (names.length === 1) return names[0] ?? "External Tool"
if (names.length === 2) {
return `${names[0] ?? "External Tool"} and ${names[1] ?? "External Tool"}`
}
return `${names.slice(0, -1).join(", ")}, and ${names.at(-1) ?? "External Tool"}`
}
function encodeBase64UrlJson(value: Record<string, string>): string {
return btoa(JSON.stringify(value))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "")
}
function PluginLogoStack({ clients }: { clients: string[] }) {
if (clients.length === 0) {
return (
<div className="flex size-10 items-center justify-center rounded-lg border border-[#1E293B] bg-[#080B0F]">
<ArrowRight className="size-5 text-[#4BA0FA]" />
</div>
)
}
return (
<div className="flex items-center justify-center">
{clients.map((id, index) => {
const plugin = PLUGIN_INFO[id]
return (
<div
className="-ml-2 flex size-10 items-center justify-center rounded-lg border border-[#1E293B] bg-[#080B0F] p-2 first:ml-0"
key={`${id}-${index}`}
style={{ zIndex: clients.length - index }}
title={plugin?.name ?? id}
>
{plugin ? (
<Image
alt={plugin.name}
className="size-6 object-contain"
height={24}
src={plugin.icon}
width={24}
/>
) : (
<ArrowRight className="size-5 text-[#4BA0FA]" />
)}
</div>
)
})}
</div>
)
}
type Status = "loading" | "creating" | "success" | "error"
const pageWrapperClass =
@ -125,9 +191,29 @@ function AuthConnectContent() {
const callback = params.get("callback")
const client = params.get("client")
const validClient = client && client in PLUGIN_INFO ? client : null
const displayName = validClient ? getPluginName(validClient) : "External Tool"
const pluginInfo = validClient ? PLUGIN_INFO[validClient] : null
const clientsParam = params.get("clients")
const hasClientList = params.has("clients")
const rawRequestedClients = useMemo(
() =>
(clientsParam !== null ? clientsParam.split(",") : client ? [client] : [])
.map((value) => value.trim())
.filter(Boolean),
[client, clientsParam],
)
const requestedClients = useMemo(
() => Array.from(new Set(rawRequestedClients.filter(isKnownPlugin))),
[rawRequestedClients],
)
const invalidClients = useMemo(
() => rawRequestedClients.filter((value) => !isKnownPlugin(value)),
[rawRequestedClients],
)
const validClient = requestedClients[0] ?? null
const displayName = formatPluginNames(requestedClients)
const pluginInfo =
requestedClients.length === 1 && validClient
? PLUGIN_INFO[validClient]
: null
// Redirect new users (logged in but no organization) to onboarding.
// Store the current connect URL so onboarding can redirect back here.
@ -163,6 +249,16 @@ function AuthConnectContent() {
setError("Invalid callback URL.")
return
}
if (invalidClients.length > 0) {
setStatus("error")
setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`)
return
}
if (requestedClients.length === 0) {
setStatus("error")
setError("Invalid or missing client.")
return
}
if (!session || !org) {
setStatus("error")
setError(
@ -174,7 +270,7 @@ function AuthConnectContent() {
try {
setStatus("creating")
const fetchParams = new URLSearchParams({ callback })
if (validClient) fetchParams.set("client", validClient)
fetchParams.set("client", requestedClients[0] ?? "")
const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, {
credentials: "include",
@ -191,7 +287,21 @@ function AuthConnectContent() {
setStatus("success")
const redirectUrl = new URL(callback)
redirectUrl.searchParams.set("apikey", data.key)
if (hasClientList) {
redirectUrl.searchParams.set(
"keys",
encodeBase64UrlJson(
Object.fromEntries(
requestedClients.map((requestedClient) => [
requestedClient,
data.key,
]),
),
),
)
} else {
redirectUrl.searchParams.set("apikey", data.key)
}
redirectUrl.searchParams.set("api_url", API_URL)
window.location.href = redirectUrl.toString()
} catch (err) {
@ -204,6 +314,20 @@ function AuthConnectContent() {
// Show a spinner while session/org data is loading or while we're about
// to redirect to onboarding (prevents a brief flash of the connect card).
const isAuthLoading = isPending || isRestoring || organizations === null
useEffect(() => {
if (status !== "loading") return
if (rawRequestedClients.length === 0) {
setStatus("error")
setError("Invalid or missing client.")
return
}
if (invalidClients.length > 0) {
setStatus("error")
setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`)
}
}, [invalidClients, rawRequestedClients.length, status])
if (isAuthLoading || shouldRedirectToOnboarding) {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
@ -217,19 +341,7 @@ function AuthConnectContent() {
<div className={pageWrapperClass}>
<div className={cardClass}>
<div className="flex flex-col items-center gap-5">
<div className="flex size-10 items-center justify-center rounded-lg border border-[#1E293B] bg-[#080B0F]">
{pluginInfo ? (
<Image
alt={pluginInfo.name}
className="size-6"
height={24}
src={pluginInfo.icon}
width={24}
/>
) : (
<ArrowRight className="size-5 text-[#4BA0FA]" />
)}
</div>
<PluginLogoStack clients={requestedClients} />
<div className="text-center">
<h2
className={dmSans125ClassName(
@ -244,13 +356,15 @@ function AuthConnectContent() {
)}
>
{pluginInfo?.description ??
`Allow ${displayName} to access your Supermemory account.`}
(requestedClients.length > 1
? "Use one Supermemory account across these plugins."
: `Use your Supermemory account with ${displayName}.`)}
</p>
</div>
{pluginInfo && (
<ul className="w-full space-y-2.5">
{pluginInfo.features.map((feature) => (
<ul className="w-full space-y-2.5">
{(pluginInfo?.features ?? MULTI_PLUGIN_FEATURES).map(
(feature) => (
<li key={feature} className="flex items-start gap-2.5">
<ArrowRight className="mt-0.5 size-3.5 shrink-0 text-[#4BA0FA]" />
<span
@ -261,16 +375,16 @@ function AuthConnectContent() {
{feature}
</span>
</li>
))}
</ul>
)}
),
)}
</ul>
<button
type="button"
onClick={handleConnect}
className={cn(
"relative w-full h-11 rounded-[10px] flex items-center justify-center",
"text-[#FAFAFA] font-medium text-[14px] tracking-[-0.14px]",
"text-[#FAFAFA] font-medium text-[14px]",
"shadow-[0px_2px_10px_rgba(5,1,0,0.2)]",
"cursor-pointer transition-opacity hover:opacity-90",
dmSans125ClassName(),
@ -290,7 +404,6 @@ function AuthConnectContent() {
</div>
)
}
if (status === "error") {
return (
<div className={pageWrapperClass}>
@ -317,7 +430,7 @@ function AuthConnectContent() {
<div className="flex flex-col gap-2 w-full">
<button
type="button"
onClick={() => window.location.reload()}
onClick={() => void handleConnect()}
className={cn(
"w-full flex items-center justify-center gap-2 rounded-full h-10 px-4",
"bg-[#0D121A] border border-[#1E293B] text-[#FAFAFA]",