mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Add plugin CLI command guide to integrations (#1534)
## Summary - add an “Install plugins with one command” action beside the Plugins section <img width="1280" height="651" alt="image" src="https://github.com/user-attachments/assets/a2e19a91-c94c-4a56-87c4-b72c4dbe89ba" /> <img width="1280" height="554" alt="image" src="https://github.com/user-attachments/assets/2cd4b3a4-ac30-4411-8221-baca09e90b0f" />
This commit is contained in:
parent
818a83a381
commit
dda56e766e
1 changed files with 306 additions and 131 deletions
|
|
@ -71,8 +71,14 @@ import {
|
|||
isFreeTierPlugin,
|
||||
normalizePluginClientId,
|
||||
type InstallStep,
|
||||
type PluginInfo,
|
||||
} from "@/lib/plugin-catalog"
|
||||
import { INSET, InstallSteps, PillButton } from "./integrations/install-steps"
|
||||
import {
|
||||
CopyButton,
|
||||
INSET,
|
||||
InstallSteps,
|
||||
PillButton,
|
||||
} from "./integrations/install-steps"
|
||||
import {
|
||||
ShortcutsConnectButtons,
|
||||
useShortcutsConnect,
|
||||
|
|
@ -638,6 +644,206 @@ function IconBox({
|
|||
)
|
||||
}
|
||||
|
||||
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<Record<string, string>> = {
|
||||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full flex-row gap-0.5 rounded-full bg-[#0D121A] p-0.5",
|
||||
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.5)]",
|
||||
)}
|
||||
role="tablist"
|
||||
aria-label="Setup method"
|
||||
>
|
||||
{(["agent", "manual"] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={cn(
|
||||
"min-h-8 flex-1 rounded-full px-3 text-center text-[12px] font-medium transition-colors",
|
||||
value === tab
|
||||
? "bg-white/[0.10] text-[#FAFAFA]"
|
||||
: "text-[#A1A1AA] hover:text-[#FAFAFA]",
|
||||
)}
|
||||
onClick={() => onChange(tab)}
|
||||
role="tab"
|
||||
type="button"
|
||||
aria-selected={value === tab}
|
||||
>
|
||||
{tab === "agent" ? "Agent instructions" : "Manual instructions"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PluginAgentInstructions({ plugin }: { plugin: PluginInfo }) {
|
||||
const prompt = pluginAgentPrompt(plugin)
|
||||
return (
|
||||
<div className="flex min-w-0 items-start gap-2 rounded-[10px] border border-white/[0.07] bg-[#0B0E13] px-3 py-2.5">
|
||||
<p className="min-w-0 flex-1 whitespace-pre-wrap break-words font-mono text-[12px] leading-[1.6] text-[#E4E4E7]">
|
||||
{prompt}
|
||||
</p>
|
||||
<CopyButton text={prompt} label="Agent instructions" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PluginCommandsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
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(),
|
||||
"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-[620px] sm:rounded-[22px]",
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="sr-only">
|
||||
Supermemory plugin commands
|
||||
</DialogTitle>
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<div
|
||||
role="img"
|
||||
aria-label="Claude Code, Cursor, Codex, and OpenCode"
|
||||
className="flex shrink-0 -space-x-2"
|
||||
>
|
||||
{PLUGIN_COMMAND_CLIENTS.map((pluginId) => {
|
||||
const plugin = PLUGIN_CATALOG[pluginId]
|
||||
if (!plugin) return null
|
||||
return (
|
||||
<span
|
||||
key={pluginId}
|
||||
className="flex size-8 items-center justify-center rounded-[9px] border border-white/[0.12] bg-[#0D121A] p-1.5 shadow-sm"
|
||||
>
|
||||
<Image
|
||||
src={plugin.icon}
|
||||
alt=""
|
||||
width={20}
|
||||
height={20}
|
||||
className="size-5 object-contain"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[16px] font-semibold leading-tight text-[#FAFAFA]">
|
||||
Plugin commands
|
||||
</p>
|
||||
<p className="mt-0.5 text-[12px] text-[#A1A1AA]">
|
||||
Install, reconnect, or remove integrations from one CLI.
|
||||
</p>
|
||||
</div>
|
||||
<DialogPrimitive.Close
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
className={cn(
|
||||
"flex size-7 shrink-0 items-center justify-center rounded-full bg-[#0D121A] transition-opacity hover:opacity-80 focus:outline-none",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
<X className="size-4 text-[#737373]" />
|
||||
</DialogPrimitive.Close>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 rounded-[14px] bg-[#14161A] p-3 sm:p-4",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
<InstallSteps steps={PLUGIN_COMMANDS} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center justify-between gap-3 pt-1">
|
||||
<p className="text-[11px] text-[#737373]">
|
||||
Run these commands from your terminal.
|
||||
</p>
|
||||
<DialogPrimitive.Close asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"flex h-9 items-center gap-1.5 rounded-full bg-[#0D121A] px-5 text-[13px] font-medium text-[#FAFAFA] transition-opacity hover:opacity-80",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
<Check className="size-3.5 text-[#4BA0FA]" /> Done
|
||||
</button>
|
||||
</DialogPrimitive.Close>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
type InfoUseCase = {
|
||||
title: string
|
||||
description: string
|
||||
|
|
@ -2478,10 +2684,12 @@ function SectionRail({
|
|||
label,
|
||||
children,
|
||||
headerSlot,
|
||||
labelSlot,
|
||||
}: {
|
||||
label: string
|
||||
children: ReactNode
|
||||
headerSlot?: ReactNode
|
||||
labelSlot?: ReactNode
|
||||
}) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false)
|
||||
|
|
@ -2523,14 +2731,17 @@ function SectionRail({
|
|||
return (
|
||||
<section className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] font-semibold tracking-[-0.01em] text-[#A1A1AA]",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</h3>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h3
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] font-semibold tracking-[-0.01em] text-[#A1A1AA]",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</h3>
|
||||
{labelSlot}
|
||||
</div>
|
||||
<div className="hidden items-center gap-1.5 sm:flex">
|
||||
{headerSlot}
|
||||
<button
|
||||
|
|
@ -2588,18 +2799,21 @@ export function IntegrationsView({
|
|||
const [connectingProvider, setConnectingProvider] =
|
||||
useState<ConnectorProvider | null>(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<PluginSetupTab>("agent")
|
||||
const openPluginSetup = useCallback((pluginId: string) => {
|
||||
setPluginSetupTab("agent")
|
||||
setNewKey({ open: true, key: "", pluginId, loading: false })
|
||||
}, [])
|
||||
const [connectedPluginId, setConnectedPluginId] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
const [finishSetupPluginId, setFinishSetupPluginId] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
const { data: pluginsData } = useQuery({
|
||||
queryFn: async () => {
|
||||
|
|
@ -2778,12 +2992,7 @@ export function IntegrationsView({
|
|||
},
|
||||
onMutate: (pluginId) => setConnectingPlugin(pluginId),
|
||||
onError: (err) => {
|
||||
// Tear down a pre-opened (loading) modal so a failed mint doesn't hang on a spinner.
|
||||
setNewKey((s) =>
|
||||
s.loading
|
||||
? { open: false, key: "", pluginId: null, loading: false }
|
||||
: s,
|
||||
)
|
||||
setNewKey((s) => ({ ...s, loading: false }))
|
||||
toast.error("Failed to connect plugin", {
|
||||
description: err instanceof Error ? err.message : "Unknown error",
|
||||
})
|
||||
|
|
@ -2793,10 +3002,32 @@ export function IntegrationsView({
|
|||
queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id] })
|
||||
},
|
||||
onSuccess: (data, pluginId) => {
|
||||
setNewKey({ open: true, key: data.key, pluginId, loading: false })
|
||||
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", {
|
||||
|
|
@ -2928,10 +3159,7 @@ export function IntegrationsView({
|
|||
void setConnectTarget(null)
|
||||
handleUpgrade("api_pro")
|
||||
} else {
|
||||
// Open instantly; the key fills in on mint. The ?connect param stays the source
|
||||
// of truth until the modal closes.
|
||||
setNewKey({ open: true, key: "", pluginId: target, loading: true })
|
||||
createPluginKeyMutation.mutate(target)
|
||||
openPluginSetup(target)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -2966,8 +3194,8 @@ export function IntegrationsView({
|
|||
redirectToLogin,
|
||||
setConnectTarget,
|
||||
setAddDoc,
|
||||
createPluginKeyMutation,
|
||||
handleUpgrade,
|
||||
openPluginSetup,
|
||||
])
|
||||
|
||||
const closeMcpModal = () => {
|
||||
|
|
@ -3282,7 +3510,7 @@ export function IntegrationsView({
|
|||
handleUpgrade("api_pro")
|
||||
return
|
||||
}
|
||||
createPluginKeyMutation.mutate("claude_code")
|
||||
openPluginSetup("claude_code")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -3360,7 +3588,7 @@ export function IntegrationsView({
|
|||
return
|
||||
}
|
||||
trackCard(item)
|
||||
createPluginKeyMutation.mutate(item.pluginId)
|
||||
openPluginSetup(item.pluginId)
|
||||
}}
|
||||
disabled={!!connectingPlugin}
|
||||
className={cn(
|
||||
|
|
@ -3381,12 +3609,7 @@ export function IntegrationsView({
|
|||
<FinishSetupButton
|
||||
onClick={() => {
|
||||
trackCard(item)
|
||||
if (!PLUGIN_CATALOG[item.pluginId]?.usesOAuth) {
|
||||
if (connectingPlugin) return
|
||||
createPluginKeyMutation.mutate(item.pluginId)
|
||||
return
|
||||
}
|
||||
setFinishSetupPluginId(item.pluginId)
|
||||
openPluginSetup(item.pluginId)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
@ -3403,7 +3626,7 @@ export function IntegrationsView({
|
|||
<PillButton
|
||||
onClick={() => {
|
||||
trackCard(item)
|
||||
createPluginKeyMutation.mutate(item.pluginId)
|
||||
openPluginSetup(item.pluginId)
|
||||
}}
|
||||
disabled={!!connectingPlugin}
|
||||
>
|
||||
|
|
@ -3573,7 +3796,7 @@ export function IntegrationsView({
|
|||
return
|
||||
}
|
||||
trackCard(item)
|
||||
createPluginKeyMutation.mutate(item.pluginId)
|
||||
openPluginSetup(item.pluginId)
|
||||
}}
|
||||
disabled={!!connectingPlugin}
|
||||
>
|
||||
|
|
@ -3686,10 +3909,6 @@ export function IntegrationsView({
|
|||
!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
|
||||
|
|
@ -3794,6 +4013,23 @@ export function IntegrationsView({
|
|||
<SectionRail
|
||||
key={cat}
|
||||
label={CATEGORY_LABEL[cat]}
|
||||
labelSlot={
|
||||
cat === "plugins" ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={pluginCommandsOpen}
|
||||
onClick={() => 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]",
|
||||
)}
|
||||
>
|
||||
<span>Install plugins with one command</span>
|
||||
<NewChip />
|
||||
</button>
|
||||
) : null
|
||||
}
|
||||
headerSlot={
|
||||
cat === "ai-clients" && activeMcpKey ? (
|
||||
<McpConnectedPill
|
||||
|
|
@ -3839,6 +4075,11 @@ export function IntegrationsView({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<PluginCommandsDialog
|
||||
open={pluginCommandsOpen}
|
||||
onOpenChange={setPluginCommandsOpen}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={newKey.open}
|
||||
onOpenChange={(open) => {
|
||||
|
|
@ -3848,7 +4089,10 @@ export function IntegrationsView({
|
|||
pluginId: open ? s.pluginId : null,
|
||||
loading: open ? s.loading : false,
|
||||
}))
|
||||
if (!open) void setConnectTarget(null)
|
||||
if (!open) {
|
||||
setPluginSetupTab("agent")
|
||||
void setConnectTarget(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
|
|
@ -3881,9 +4125,11 @@ export function IntegrationsView({
|
|||
Set up {dialogPlugin?.name ?? "your plugin"}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-[12px] text-[#A1A1AA]">
|
||||
{newKey.loading
|
||||
? "Generating your key…"
|
||||
: "Copy your key and run these steps to finish."}
|
||||
{pluginSetupTab === "agent"
|
||||
? "Copy this prompt into your coding agent."
|
||||
: newKey.loading
|
||||
? "Generating your key…"
|
||||
: "Follow these steps to finish manually."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
|
|
@ -3916,17 +4162,30 @@ export function IntegrationsView({
|
|||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 rounded-[14px] bg-[#14161A] p-4 sm:p-5",
|
||||
"min-w-0 space-y-4 rounded-[14px] bg-[#14161A] p-4 sm:p-5",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
{newKey.loading ? (
|
||||
<PluginSetupMethodTabs
|
||||
value={pluginSetupTab}
|
||||
onChange={selectPluginSetupTab}
|
||||
/>
|
||||
{pluginSetupTab === "agent" && dialogPlugin ? (
|
||||
<PluginAgentInstructions plugin={dialogPlugin} />
|
||||
) : newKey.loading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-10 text-[13px] text-[#A1A1AA]">
|
||||
<Loader className="size-4 animate-spin" />
|
||||
Generating your key…
|
||||
</div>
|
||||
) : (
|
||||
) : newKey.key ? (
|
||||
<InstallSteps steps={setupSteps} apiKey={newKey.key} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
||||
<p className="text-[13px] text-[#A1A1AA]">
|
||||
We couldn't generate the key for the manual setup.
|
||||
</p>
|
||||
<PillButton onClick={generatePluginKey}>Try again</PillButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3940,6 +4199,7 @@ export function IntegrationsView({
|
|||
pluginId: null,
|
||||
loading: false,
|
||||
})
|
||||
setPluginSetupTab("agent")
|
||||
void setConnectTarget(null)
|
||||
}}
|
||||
className={cn(
|
||||
|
|
@ -4078,7 +4338,7 @@ export function IntegrationsView({
|
|||
if (!connectedPluginId) return
|
||||
const pluginId = connectedPluginId
|
||||
setConnectedPluginId(null)
|
||||
createPluginKeyMutation.mutate(pluginId)
|
||||
openPluginSetup(pluginId)
|
||||
}}
|
||||
disabled={!!connectingPlugin}
|
||||
>
|
||||
|
|
@ -4109,91 +4369,6 @@ export function IntegrationsView({
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={!!finishSetupPluginId}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setFinishSetupPluginId(null)
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
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(),
|
||||
"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 rounded-2xl md:px-4 sm:max-w-[560px] sm:rounded-[22px]",
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="sr-only">
|
||||
Finish setup {finishSetupPlugin?.name ?? "plugin"}
|
||||
</DialogTitle>
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
{finishSetupPlugin && (
|
||||
<IconBox>
|
||||
<Image
|
||||
src={finishSetupPlugin.icon}
|
||||
alt={finishSetupPlugin.name}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</IconBox>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-[16px] font-semibold leading-tight text-[#FAFAFA]">
|
||||
Finish setup {finishSetupPlugin?.name ?? "plugin"}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-[12px] text-[#A1A1AA]">
|
||||
Complete install in the tool — this card turns active after the
|
||||
first API call.
|
||||
</p>
|
||||
</div>
|
||||
<DialogPrimitive.Close
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
className={cn(
|
||||
"flex size-7 items-center justify-center rounded-full bg-[#0D121A] transition-opacity hover:opacity-80 focus:outline-none",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
<X className="size-4 text-[#737373]" />
|
||||
</DialogPrimitive.Close>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 rounded-[14px] bg-[#14161A] p-4 sm:p-5",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
{finishSetupSteps.length > 0 ? (
|
||||
<InstallSteps steps={finishSetupSteps} />
|
||||
) : (
|
||||
<p className="text-[13px] text-[#A1A1AA]">
|
||||
Open {finishSetupPlugin?.name ?? "the plugin"} and finish
|
||||
authentication, then send a test memory.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center justify-end">
|
||||
<DialogPrimitive.Close asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"flex h-9 items-center gap-1.5 rounded-full bg-[#0D121A] px-5 text-[13px] font-medium text-[#FAFAFA] transition-opacity hover:opacity-80",
|
||||
INSET,
|
||||
)}
|
||||
>
|
||||
<Check className="size-3.5 text-[#4BA0FA]" /> Done
|
||||
</button>
|
||||
</DialogPrimitive.Close>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={mcpModalOpen}
|
||||
onOpenChange={(open) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue