From 085c3dd8b3dd0e9b06b7120a73451d7875c11e3e Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 11 May 2026 23:13:08 +0000 Subject: [PATCH] feat: add codex and revamp plugins page (#931) --- .../entrypoints/content/t3.ts | 3 +- .../integrations/plugins-detail.tsx | 1003 +++++++++++------ apps/web/components/settings/account.tsx | 32 +- apps/web/globals.css | 8 + apps/web/public/images/plugins/codex.png | Bin 0 -> 7578 bytes 5 files changed, 671 insertions(+), 375 deletions(-) create mode 100644 apps/web/public/images/plugins/codex.png diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts index e0b97e3f..c7bdb09a 100644 --- a/apps/browser-extension/entrypoints/content/t3.ts +++ b/apps/browser-extension/entrypoints/content/t3.ts @@ -238,8 +238,7 @@ async function getRelatedMemoriesForT3(actionSource: string) { } if (textareaElement) { - textareaElement.dataset.supermemories = - `\n\nSupermemories of user (only for the reference): ${response.data}` + textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` iconElement.dataset.memoriesData = response.data diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx index c7b43f23..ade962ae 100644 --- a/apps/web/components/integrations/plugins-detail.tsx +++ b/apps/web/components/integrations/plugins-detail.tsx @@ -7,98 +7,161 @@ import { useAuth } from "@lib/auth-context" 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 { - ArrowRight, BookOpen, Check, - CheckCircle, + ChevronDown, Copy, - ExternalLink, Loader, - Trash2, + X, Zap, } from "lucide-react" import Image from "next/image" -import { useMemo, useState } from "react" +import { type ReactNode, useEffect, useMemo, useState } from "react" import { toast } from "sonner" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogPortal, -} from "@ui/components/dialog" +import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" +import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" + +/** Recessed "inside-out" inset shadow used across Supermemory surfaces. */ +const INSET = + "shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]" /** Match `FREE_TIER_PLUGIN_IDS` in mono `packages/lib/plugins.ts`. */ +const FREE_TIER_PLUGIN_IDS = ["hermes", "codex"] function isFreeTierPlugin(pluginId: string): boolean { - return pluginId === "hermes" + return FREE_TIER_PLUGIN_IDS.includes(pluginId) +} + +interface InstallStep { + title: string + description?: string + code?: string + copyLabel?: string + optional?: boolean + /** Blur the code block until hovered/focused (e.g. it contains the key). */ + secret?: boolean } interface PluginInfo { id: string name: string - description: string - features: string[] + tagline: string icon: string docsUrl?: string - repoUrl?: string + /** Steps shown after a key is minted. The literal `sm_...` is replaced + * with the freshly generated key when rendered. */ + installSteps?: InstallStep[] } const PLUGIN_CATALOG: Record = { claude_code: { id: "claude_code", name: "Claude Code", - description: - "Claude Code remembers your conventions, past decisions, and project context across every session — no re-explaining yourself.", - features: [ - "Picks up where you left off at session start", - "Captures decisions and patterns from tool usage", - "Builds a persistent profile of how you work", - ], + tagline: "Remembers your conventions, decisions, and project context", icon: "/images/plugins/claude-code.svg", docsUrl: "https://docs.supermemory.ai/integrations/claude-code", - repoUrl: "https://github.com/supermemoryai/claude-supermemory", + installSteps: [ + { + title: "Save your API key", + description: + "Add this to your shell profile so Claude Code can authenticate. This key is shown only once — save it now.", + code: 'export SUPERMEMORY_CC_API_KEY="sm_..."', + copyLabel: "API key", + secret: true, + }, + { + title: "Install the plugin", + description: "Run these commands inside a Claude Code session:", + code: "/plugin marketplace add supermemoryai/claude-supermemory\n/plugin install claude-supermemory", + }, + ], + }, + codex: { + id: "codex", + name: "Codex", + tagline: "Persistent memory for the Codex CLI — free on every plan", + icon: "/images/plugins/codex.png", + docsUrl: "https://docs.supermemory.ai/integrations/codex", + 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_CODEX_API_KEY="sm_..."', + copyLabel: "API key", + secret: true, + }, + { + title: "Install the hooks", + description: "Run this to wire Supermemory into Codex CLI:", + code: "npx codex-supermemory@latest install", + }, + ], }, opencode: { id: "opencode", name: "OpenCode", - description: - "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 injected before each prompt", - ], + tagline: "Long-term memory for your OpenCode sessions", icon: "/images/plugins/opencode.svg", docsUrl: "https://docs.supermemory.ai/integrations/opencode", + 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: "Verify your config", + description: + "Ensure ~/.config/opencode/opencode.jsonc includes the plugin:", + code: '{\n "plugin": ["opencode-supermemory"]\n}', + optional: true, + }, + ], }, openclaw: { id: "openclaw", name: "OpenClaw", - description: - "Persists memory across Telegram, WhatsApp, Discord, and Slack. OpenClaw knows who users are and what they talked about before.", - features: [ - "Cross-channel memory that follows the user", - "Automatic conversation capture", - "User profiles built across every platform", - ], + tagline: "Cross-platform memory across Telegram, Discord, Slack", icon: "/images/plugins/openclaw.svg", docsUrl: "https://docs.supermemory.ai/integrations/openclaw", - repoUrl: "https://github.com/supermemoryai/openclaw-supermemory", + installSteps: [ + { + title: "Install the plugin", + description: "Run this in your OpenClaw project:", + code: "openclaw plugins install @supermemory/openclaw-supermemory", + }, + { + title: "Configure Supermemory", + description: + "Run the setup command and paste your API key when prompted:", + code: "openclaw supermemory setup", + }, + ], }, hermes: { id: "hermes", name: "Hermes", - 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", - "Persistent user profile built over time", - ], + tagline: "Persistent memory for the Hermes agent — free on every plan", icon: "/images/plugins/hermes.svg", docsUrl: "https://docs.supermemory.ai/integrations/hermes", - repoUrl: "https://github.com/NousResearch/hermes-agent", + installSteps: [ + { + title: "Run Hermes memory setup", + description: + "On the machine where Hermes is deployed, start the memory wizard, choose Supermemory as the provider, and paste your API key when prompted:", + code: "hermes memory setup", + }, + ], }, } @@ -107,284 +170,447 @@ interface ConnectedPlugin { keyId: string pluginId: string createdAt: string - lastUsed?: string | null keyStart?: string | null } -function ProUpgradeNudge({ onUpgrade }: { onUpgrade: () => void }) { +function SectionHeader({ children }: { children: ReactNode }) { return ( -
-
- -

- Unlock Claude Code, OpenCode, OpenClaw and more with{" "} - Pro -

-
- +

+ {children} +

+ ) +} + +function PluginIconBox({ + src, + alt, + dimmed, +}: { + src: string + alt: string + dimmed?: boolean +}) { + return ( +
+ {alt}
) } -function ConnectedPluginRow({ - plugin, - info, +function ProChip() { + return ( + + Pro + + ) +} + +function PillButton({ + children, + onClick, + disabled, +}: { + children: ReactNode + onClick?: () => void + disabled?: boolean +}) { + return ( + + ) +} + +function DocsLink({ href }: { href: string }) { + return ( + + Docs + + ) +} + +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 ConnectedPill({ + connectedKeys, onRevoke, }: { - plugin: ConnectedPlugin - info: PluginInfo | undefined + connectedKeys: ConnectedPlugin[] onRevoke: (keyId: string) => void }) { return ( -
-
- {info && ( -
- {info.name} -
+ + + + + -

- {info?.name || plugin.pluginId} -

-
-
- +

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

+
+ {connectedKeys.map((k) => ( +
- Connected - - {plugin.keyStart && ( - {plugin.keyStart}… + {k.keyStart ? `${k.keyStart}…` : "API key"} - )} -
+ onRevoke(k.keyId)} /> +
+ ))}
- -
-
+ + ) } -function PluginCard({ +function PluginRow({ plugin, pluginId, - isConnected, - isCurrentlyConnecting, - connectingPlugin, + connectedKeys, needsProUpgrade, + isConnecting, + actionsDisabled, onConnect, onUpgrade, + onRevoke, }: { plugin: PluginInfo pluginId: string - isConnected: boolean - isCurrentlyConnecting: boolean - connectingPlugin: string | null + connectedKeys: ConnectedPlugin[] needsProUpgrade: boolean + isConnecting: boolean + actionsDisabled: boolean onConnect: (id: string) => void onUpgrade: () => void + onRevoke: (keyId: string) => void }) { + const isConnected = connectedKeys.length > 0 return ( -
-
-
+ +
+
+ {isConnected && ( + + )} + + {plugin.name} + + {!isConnected && needsProUpgrade && } +
+

- {plugin.name} -

-
-
- - {plugin.name} - - {isConnected && ( - - Connected - - )} - {needsProUpgrade && ( - - PRO - - )} -
-

- {plugin.description} -

-
+ {plugin.tagline} +

- -
    - {plugin.features.map((feature) => ( -
  • - - - {feature} - -
  • - ))} -
- -
+
+ {plugin.docsUrl && } {isConnected ? ( - + ) : needsProUpgrade ? ( - + + Upgrade + ) : ( - + )} -
- {plugin.docsUrl && ( - - Docs - - )} - {plugin.repoUrl && ( - - GitHub - - )} -
) } +type TierFilter = "all" | "pro" | "free" + +const TIER_FILTERS: { value: TierFilter; label: string }[] = [ + { value: "all", label: "All" }, + { value: "pro", label: "Pro" }, + { value: "free", label: "Free" }, +] + +function TierFilterToggle({ + value, + onChange, +}: { + value: TierFilter + onChange: (value: TierFilter) => void +}) { + return ( +
+ {TIER_FILTERS.map((filter) => ( + + ))} +
+ ) +} + +function CopyButton({ text, label }: { text: string; label?: string }) { + const [copied, setCopied] = useState(false) + return ( + + ) +} + +function CodeBlock({ + code, + copyLabel = "Command", + secret, +}: { + code: string + copyLabel?: string + secret?: boolean +}) { + return ( +
+
+				{code}
+			
+ +
+ ) +} + +function InstallSteps({ + steps, + apiKey, +}: { + steps: InstallStep[] + apiKey: string +}) { + return ( +
    + {steps.map((step, i) => ( +
  1. +
    + + {i + 1} + + {i < steps.length - 1 && ( + + )} +
    +
    +
    +
    +

    + {step.title} +

    + {step.optional && ( + + Optional + + )} +
    + {step.description && ( +

    + {step.description} +

    + )} +
    + {step.code && ( + + )} +
    +
  2. + ))} +
+ ) +} + export function PluginsDetail() { const { org } = useAuth() const autumn = useCustomer() const queryClient = useQueryClient() + const [tierFilter, setTierFilter] = useState("all") const [connectingPlugin, setConnectingPlugin] = useState(null) - const [newKey, setNewKey] = useState<{ open: boolean; key: string }>({ + const [newKey, setNewKey] = useState<{ + open: boolean + key: string + pluginId: string | null + }>({ open: false, key: "", + pluginId: null, }) - const [keyCopied, setKeyCopied] = useState(false) const hasProProduct = hasActivePlan(autumn.data?.subscriptions, "api_pro") @@ -432,7 +658,6 @@ export function PluginsDetail() { keyId: key.id, pluginId: metadata.sm_client, createdAt: key.createdAt.toISOString(), - lastUsed: key.lastRequest?.toISOString() ?? null, keyStart: key.start ?? null, }) } @@ -442,7 +667,7 @@ export function PluginsDetail() { }, [apiKeys]) const connectedPluginIds = useMemo( - () => connectedPlugins.map((p) => p.pluginId), + () => new Set(connectedPlugins.map((p) => p.pluginId)), [connectedPlugins], ) @@ -477,9 +702,8 @@ export function PluginsDetail() { setConnectingPlugin(null) queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id] }) }, - onSuccess: (data) => { - setNewKey({ open: true, key: data.key }) - toast.success("Plugin connected!") + onSuccess: (data, pluginId) => { + setNewKey({ open: true, key: data.key, pluginId }) }, }) @@ -510,91 +734,98 @@ export function PluginsDetail() { } } - const handleCopyKey = async () => { - try { - await navigator.clipboard.writeText(newKey.key) - setKeyCopied(true) - setTimeout(() => setKeyCopied(false), 2000) - toast.success("API key copied!") - } catch { - toast.error("Failed to copy") - } - } - const isLoading = autumn.isLoading const availablePlugins = pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG) - const allCatalogPluginIds = useMemo( + const catalogRows = useMemo( () => availablePlugins.filter((id) => PLUGIN_CATALOG[id]), [availablePlugins], ) + const visibleRows = useMemo(() => { + const filtered = catalogRows.filter((id) => { + if (tierFilter === "free") return isFreeTierPlugin(id) + if (tierFilter === "pro") return !isFreeTierPlugin(id) + return true + }) + // Connected plugins float to the top (stable within each group). + return [...filtered].sort( + (a, b) => + Number(connectedPluginIds.has(b)) - Number(connectedPluginIds.has(a)), + ) + }, [catalogRows, tierFilter, connectedPluginIds]) + + const dialogPlugin = newKey.pluginId + ? PLUGIN_CATALOG[newKey.pluginId] + : undefined + + 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. + const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_...")) + const setupSteps: InstallStep[] = stepsEmbedKey + ? 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 ( <>
- {!hasProProduct && !isLoading && ( - - )} - - {connectedPlugins.length > 0 && ( -
- - Connected - - {connectedPlugins.map((plugin) => ( - - ))} -
- )} -
- + Plugins + {catalogRows.length > 0 && ( + )} - > - {connectedPlugins.length > 0 - ? "Add more plugins" - : "Available plugins"} - -
- {allCatalogPluginIds.map((pluginId) => { +
+
+ {visibleRows.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 ( - p.pluginId === pluginId, + )} needsProUpgrade={needsProUpgrade} + isConnecting={connectingPlugin === pluginId} + actionsDisabled={!!connectingPlugin} onConnect={(id) => createPluginKeyMutation.mutate(id)} onUpgrade={handleUpgrade} + onRevoke={handleRevoke} /> ) })} + {visibleRows.length === 0 && ( +

+ No plugins in this category. +

+ )}
@@ -602,65 +833,105 @@ export function PluginsDetail() { - setNewKey({ open, key: open ? newKey.key : "" }) + setNewKey((s) => ({ + open, + key: open ? s.key : "", + pluginId: open ? s.pluginId : null, + })) } > - - - - + + Set up {dialogPlugin?.name ?? "your plugin"} + + +
+ {dialogPlugin && ( + + )} +
+

- Plugin Connected - - -

-

- Save your API key now: you won't be able to see it again. + Set up {dialogPlugin?.name ?? "your plugin"}

-
- - -
- + Copy your key and run these steps to finish. +

- - +
+ {dialogPlugin?.docsUrl && ( + + Docs + + )} + + + +
+
+ +
+
+ +
+
+ +
+ +
+
) diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 30227779..a7ae2d63 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -188,14 +188,21 @@ function formatOrgRole(role: string): string { } export default function Account() { - const { user, org, setActiveOrg, clearActiveOrg } = useAuth() + const { + user, + org, + organizations: allOrgs, + setActiveOrg, + clearActiveOrg, + } = useAuth() const autumn = useCustomer() const [isUpgrading, setIsUpgrading] = useState(false) const [emailConfirm, setEmailConfirm] = useState("") const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const [isClosingAccount, setIsClosingAccount] = useState(false) const [switchingOrgId, setSwitchingOrgId] = useState(null) - const { data: allOrgs } = authClient.useListOrganizations() + const [orgMenuOpen, setOrgMenuOpen] = useState(false) + const canSwitchOrg = (allOrgs?.length ?? 0) > 1 const { data: memberships, isPending: membershipsPending } = useAccountMemberships() @@ -363,10 +370,19 @@ export default function Account() { > Organization

- + { + if (canSwitchOrg) setOrgMenuOpen(open) + }} + > @@ -378,14 +394,16 @@ export default function Account() { > {org?.name ?? "Personal"} - + {canSwitchOrg && ( + + )} - {allOrgs && allOrgs.length > 1 && ( + {canSwitchOrg && ( - {allOrgs.map((organization) => { + {allOrgs?.map((organization) => { const isCurrent = organization.id === org?.id const isSwitching = switchingOrgId === organization.id return ( diff --git a/apps/web/globals.css b/apps/web/globals.css index 283c51f2..ac424c36 100644 --- a/apps/web/globals.css +++ b/apps/web/globals.css @@ -65,6 +65,14 @@ scrollbar-color: var(--sm-scrollbar-thumb) transparent; } +.scrollbar-none { + scrollbar-width: none; +} + +.scrollbar-none::-webkit-scrollbar { + display: none; +} + .sm-tweet-theme .react-tweet-theme { --tweet-container-margin: 0px; font-size: inherit !important; diff --git a/apps/web/public/images/plugins/codex.png b/apps/web/public/images/plugins/codex.png new file mode 100644 index 0000000000000000000000000000000000000000..11155e4d8d043c5f0c64256501150001b1c659aa GIT binary patch literal 7578 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 literal 0 HcmV?d00001