From be267c2fc8330789a18673a5631495f2ddd004ca Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:02:50 +0000 Subject: [PATCH 01/24] feat(web): automation connection warnings and calmer automations page (#1396) Inline notice with app icons when a channel automation can't use personal-only connections (footer, next to Save), post-save warning toast from the API, templates capped to 3 connection-relevant ideas with a show-all toggle, and New automation promoted to a primary button on the heading row. Pairs with mono #2724; degrades gracefully without it. Fixes ENG-1151 --- apps/web/components/configure-view.tsx | 23 ++- .../settings/company-brain-automations.tsx | 182 +++++++++++++++--- 2 files changed, 172 insertions(+), 33 deletions(-) diff --git a/apps/web/components/configure-view.tsx b/apps/web/components/configure-view.tsx index 83f43c3e..eaa129ed 100644 --- a/apps/web/components/configure-view.tsx +++ b/apps/web/components/configure-view.tsx @@ -162,16 +162,19 @@ export function ConfigureView() {
-
-

- {active.label} -

-

- {active.description} -

+
+
+

+ {active.label} +

+

+ {active.description} +

+
+
onDone: () => void onCancelNew?: () => void onCollapse?: () => void @@ -349,9 +358,18 @@ function AutomationCard({ const b = (await res.json().catch(() => ({}))) as { error?: string } throw new Error(b.error ?? "Couldn't save.") } + const b = (await res.json().catch(() => ({}))) as { + warnings?: { app: string }[] + } + return b.warnings ?? [] }, - onSuccess: () => { + onSuccess: (warnings) => { toast.success("Automation saved.") + if (warnings.length) + toast.warning( + `Heads up: ${warnings.map((w) => w.app).join(", ")} ${warnings.length === 1 ? "is" : "are"} connected personally and won't be available to this channel automation. ${isAdmin ? "Reconnect it for the workspace in Connections." : "Ask an admin to connect it for the workspace."}`, + { duration: 10000 }, + ) onDone() }, onError: (err) => @@ -619,6 +637,51 @@ function AutomationCard({ Cancel ) : null} + + {draft.deliverTo === "channel" && personalOnlyApps.length > 0 && ( + + + + {personalOnlyApps.map((app) => ( + + + {appCatalog[app]?.iconDomain ? ( + {appCatalog[app]?.name + ) : ( + + {app.slice(0, 1)} + + )} + + + {appCatalog[app]?.name ?? app} + + + ))} + + + + only connected to you ·{" "} + {isAdmin ? ( + <> + + Connect for workspace + {" "} + to use here + + ) : ( + "ask an admin to connect it for the workspace" + )} + + + )}
{id ? ( @@ -851,8 +914,14 @@ function PresetCard({ export default function CompanyBrainAutomations() { const isCompanyBrain = useHasCompanyBrain() const { user, org } = useAuth() + const { isAdmin } = useOrgMemberRole(isCompanyBrain) const queryClient = useQueryClient() const [drafts, setDrafts] = useState<{ key: number; draft: Draft }[]>([]) + const [showAllTemplates, setShowAllTemplates] = useState(false) + const [actionSlot, setActionSlot] = useState(null) + useEffect(() => { + setActionSlot(document.getElementById("configure-section-actions")) + }, []) const [openId, setOpenId] = useState(null) const draftKey = useRef(0) const addDraft = (draft: Draft) => @@ -886,11 +955,15 @@ export default function CompanyBrainAutomations() { const res = await fetch(`${BACKEND}/brain/mcp-connections/`, { credentials: "include", }) - if (!res.ok) return [] as string[] + if (!res.ok) return [] as { serverSlug: string; userId: string | null }[] const body = (await res.json()) as { - connections?: { serverSlug: string }[] + connections?: { + serverSlug: string + userId: string | null + status: string + }[] } - return (body.connections ?? []).map((c) => c.serverSlug) + return (body.connections ?? []).filter((c) => c.status === "active") }, enabled: isCompanyBrain, }) @@ -899,7 +972,39 @@ export default function CompanyBrainAutomations() { const channels = channelsQuery.data ?? [] const automations = listQuery.data ?? [] - const presets = sortPresets(new Set(appsQuery.data ?? [])) + const catalogQuery = useQuery({ + queryKey: ["company-brain-automations", "catalog", "v2"], + queryFn: async () => { + const res = await fetch(`${BACKEND}/brain/mcp-connections/catalog`, { + credentials: "include", + }) + if (!res.ok) + return {} as Record + const body = (await res.json()) as { + catalog?: { slug: string; name?: string; iconDomain?: string }[] + } + return Object.fromEntries( + (body.catalog ?? []).map((e) => [ + e.slug, + { name: e.name ?? e.slug, iconDomain: e.iconDomain }, + ]), + ) + }, + enabled: isCompanyBrain, + }) + + const connections = appsQuery.data ?? [] + const presets = sortPresets(new Set(connections.map((c) => c.serverSlug))) + const sharedApps = new Set( + connections.filter((c) => c.userId === null).map((c) => c.serverSlug), + ) + const personalOnlyApps = [ + ...new Set( + connections + .filter((c) => c.userId !== null && !sharedApps.has(c.serverSlug)) + .map((c) => c.serverSlug), + ), + ] const nameFor = (userId: string | null): string | undefined => { if (!userId) return undefined if (userId === user?.id) return "You" @@ -913,10 +1018,34 @@ export default function CompanyBrainAutomations() { } const usedTitles = new Set(automations.map((a) => a.title)) const availablePresets = presets.filter((p) => !usedTitles.has(p.label)) + const shownPresets = showAllTemplates + ? availablePresets + : availablePresets.slice(0, 3) + const hiddenTemplateCount = availablePresets.length - shownPresets.length const hasList = automations.length > 0 || drafts.length > 0 + const newAutomationButton = ( + + ) + const newAutomationPortal = actionSlot ? ( + createPortal(newAutomationButton, actionSlot) + ) : ( +
{newAutomationButton}
+ ) + return (
+ {newAutomationPortal}
{automations.map((a) => openId === a.id ? ( @@ -925,6 +1054,9 @@ export default function CompanyBrainAutomations() { id={a.id} initial={toDraft(a)} channels={channels} + personalOnlyApps={personalOnlyApps} + isAdmin={isAdmin} + appCatalog={catalogQuery.data ?? {}} onDone={() => { setOpenId(null) refresh() @@ -953,6 +1085,9 @@ export default function CompanyBrainAutomations() { id={null} initial={draft} channels={channels} + personalOnlyApps={personalOnlyApps} + isAdmin={isAdmin} + appCatalog={catalogQuery.data ?? {}} onDone={() => { removeDraft(key) refresh() @@ -961,37 +1096,38 @@ export default function CompanyBrainAutomations() { /> ))} - {hasList ? ( -

- Templates -

- ) : null} +

+ {showAllTemplates ? "Templates" : "Ideas for your setup"} +

- {availablePresets.map((p) => ( + {shownPresets.map((p) => ( addDraft(presetToDraft(p))} /> ))} +
+ {hiddenTemplateCount > 0 || showAllTemplates ? ( -
+ ) : null}
) From 59b148e5b2d4f5b4e27c9a6351fb3a0224ed76f2 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:10:46 +0000 Subject: [PATCH 02/24] feat(web): sell Company Brain on Max as well as Scale (#1440) Company Brain workspaces could only buy Scale at $399/mo, which is roughly eight times what the median team uses. Adds the $100/mo Max card to the Company Brain plan picker, notes what a Scale trial loses on the way down, and flags that Scale is cheaper above about $400/mo of credits. --- .../onboarding-brain/step-sources.tsx | 22 +++++-- apps/web/components/settings/billing.tsx | 65 +++++++++++++++++-- apps/web/hooks/use-connector-access.ts | 3 + 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/apps/web/components/onboarding-brain/step-sources.tsx b/apps/web/components/onboarding-brain/step-sources.tsx index 358597e6..5535392d 100644 --- a/apps/web/components/onboarding-brain/step-sources.tsx +++ b/apps/web/components/onboarding-brain/step-sources.tsx @@ -97,7 +97,7 @@ type SourceId = | "raycast" type SourceState = "idle" | "connecting" | "connected" | "waitlist" type DriveScope = "selective" | "full" -type RequiredPlan = "pro" | "max" +type RequiredPlan = "pro" | "max" | "scale" const PROVIDER_TO_SOURCE: Record = { "google-drive": "drive", @@ -116,6 +116,7 @@ const SOURCE_LABEL: Partial> = { const PLAN_LABELS: Record = { pro: "Pro", max: "Max", + scale: "Scale", } const BOOK_CALL_HREF = "https://cal.com/maheshthedev/15min" @@ -277,7 +278,12 @@ export function StepSources({ const [granolaOpen, setGranolaOpen] = useState(false) const [requestedPlan, setRequestedPlan] = useState("pro") const [requestedConnector, setRequestedConnector] = useState("This connector") - const { hasMax, connectorAccess, loading: planLoading } = useConnectorAccess() + const { + hasMax, + hasScale, + connectorAccess, + loading: planLoading, + } = useConnectorAccess() const { org, isRestoring } = useAuth() useEffect(() => { @@ -362,10 +368,12 @@ export function StepSources({ } }, [connectedParam]) - // company_brain unlocks pro connectors; max stays gated + // company_brain unlocks pro connectors; max and scale stay gated, and a + // higher tier satisfies a lower requirement. const isLocked = (plan?: RequiredPlan) => { if (!plan || planLoading) return false - if (plan === "max") return !hasMax + if (plan === "scale") return !hasScale + if (plan === "max") return !(hasMax || hasScale) return !connectorAccess } @@ -1185,14 +1193,14 @@ function MoreSourcesGrid({ icon={} state={values.connected.github ?? "idle"} ctaLabel="Connect" - locked={isLocked("max")} - requiredPlan="max" + locked={isLocked("scale")} + requiredPlan="scale" perks={[ "PRs and issues parsed", "READMEs and docs indexed", "Stays in sync with new activity", ]} - onConnect={guard("max", "GitHub", () => requestWaitlist("github"))} + onConnect={guard("scale", "GitHub", () => requestWaitlist("github"))} /> {mode === "personal" ? ( handleUpgrade("api_max")} + disabled={disabled} + className={cn( + dmSans125ClassName(), + PLAN_CARD_ACTION_CLASS, + "bg-[#0054AD] text-[#FAFAFA] hover:bg-[#0B65C9]", + )} + > + {disabled ? : null} + Activate Max + + ) + } + // Trial Scale: primary CTA is activate paid Scale (not a dead "current" state). if (plan.id === "scale" && (isOnTrial || isBrainTrialEnded)) { return ( @@ -1471,6 +1508,20 @@ export default function Billing() { } /> ))} +
+ {isOnTrial ? ( +

+ Your trial runs on Scale. Moving to Max keeps the agent, + shared memory and unlimited seats, and drops the GitHub, S3 + and Web Crawler connectors, restricted access and container + tags, and User Insights. +

+ ) : null} +

+ Using more than about $400 of credits a month? Scale works out + cheaper than Max plus top-ups. +

+
) : ( <> diff --git a/apps/web/hooks/use-connector-access.ts b/apps/web/hooks/use-connector-access.ts index 9f2599c3..d597ee1a 100644 --- a/apps/web/hooks/use-connector-access.ts +++ b/apps/web/hooks/use-connector-access.ts @@ -9,9 +9,12 @@ export function useConnectorAccess(opts?: { enabled?: boolean }) { const hasCompanyBrain = useHasCompanyBrain() const hasPro = enabled && hasActivePlan(autumn.data?.subscriptions, "api_pro") const hasMax = enabled && hasActivePlan(autumn.data?.subscriptions, "api_max") + const hasScale = + enabled && hasActivePlan(autumn.data?.subscriptions, "api_scale") return { hasPro, hasMax, + hasScale, hasCompanyBrain, connectorAccess: hasPro || hasCompanyBrain, loading: enabled && autumn.isLoading, From a7efd817ec22a97c935d89c811121327feb8080e Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 19:35:45 +0530 Subject: [PATCH 03/24] fix(validation): reject non-positive page/limit in pagination query schemas (#1271) --- packages/validation/api.test.ts | 74 ++++++++++++++++++++++++++++++++- packages/validation/api.ts | 10 ++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/packages/validation/api.test.ts b/packages/validation/api.test.ts index 05c18934..e186af88 100644 --- a/packages/validation/api.test.ts +++ b/packages/validation/api.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "bun:test" import { readFileSync } from "node:fs" -import { SearchRequestSchema, Searchv4RequestSchema } from "./api" +import { + DocumentsWithMemoriesQuerySchema, + ListMemoriesQuerySchema, + SearchRequestSchema, + Searchv4RequestSchema, +} from "./api" describe("search threshold schemas", () => { it("do not contain redundant number transforms or unreachable range guards", () => { @@ -80,3 +85,70 @@ describe("search threshold schemas", () => { ).toBe(false) }) }) + +describe("pagination query schemas", () => { + it("preserve page/limit defaults", () => { + const listed = ListMemoriesQuerySchema.parse({}) + expect(listed.page).toBe(1) + expect(listed.limit).toBe(10) + + const docs = DocumentsWithMemoriesQuerySchema.parse({}) + expect(docs.page).toBe(1) + expect(docs.limit).toBe(10) + }) + + it.each([ + 1, 50, 1100, + ])("ListMemoriesQuerySchema accepts numeric limit %p", (limit) => { + expect(ListMemoriesQuerySchema.parse({ limit }).limit).toBe(limit) + }) + + it("ListMemoriesQuerySchema accepts numeric string page/limit", () => { + const parsed = ListMemoriesQuerySchema.parse({ page: "3", limit: "25" }) + expect(parsed.page).toBe(3) + expect(parsed.limit).toBe(25) + }) + + it.each([ + 0, -5, 2.5, + ])("ListMemoriesQuerySchema rejects non-positive or fractional numeric limit %p", (limit) => { + expect(ListMemoriesQuerySchema.safeParse({ limit }).success).toBe(false) + }) + + it.each([ + 0, -1, 1.5, + ])("ListMemoriesQuerySchema rejects non-positive or fractional numeric page %p", (page) => { + expect(ListMemoriesQuerySchema.safeParse({ page }).success).toBe(false) + }) + + it("ListMemoriesQuerySchema still caps limit at 1100", () => { + expect(ListMemoriesQuerySchema.safeParse({ limit: 1101 }).success).toBe( + false, + ) + }) + + it.each([ + 0, -1, 2.5, + ])("DocumentsWithMemoriesQuerySchema rejects invalid page %p", (page) => { + expect(DocumentsWithMemoriesQuerySchema.safeParse({ page }).success).toBe( + false, + ) + }) + + it.each([ + 0, -10, 2.5, + ])("DocumentsWithMemoriesQuerySchema rejects invalid limit %p", (limit) => { + expect(DocumentsWithMemoriesQuerySchema.safeParse({ limit }).success).toBe( + false, + ) + }) + + it("DocumentsWithMemoriesQuerySchema accepts a normal request", () => { + const parsed = DocumentsWithMemoriesQuerySchema.parse({ + page: 2, + limit: 50, + }) + expect(parsed.page).toBe(2) + expect(parsed.limit).toBe(50) + }) +}) diff --git a/packages/validation/api.ts b/packages/validation/api.ts index f689dbf7..f066bfcd 100644 --- a/packages/validation/api.ts +++ b/packages/validation/api.ts @@ -275,6 +275,9 @@ export const ListMemoriesQuerySchema = z .regex(/^\d+$/) .or(z.number()) .transform(Number) + .refine((value) => Number.isInteger(value) && value >= 1, { + message: "Limit must be a positive integer", + }) .refine((value) => value <= 1100, { message: "Limit cannot be greater than 1100", }) @@ -292,6 +295,9 @@ export const ListMemoriesQuerySchema = z .regex(/^\d+$/) .or(z.number()) .transform(Number) + .refine((value) => Number.isInteger(value) && value >= 1, { + message: "Page must be a positive integer", + }) .default("1") .openapi({ description: "Page number to fetch", example: "1" }), sort: z @@ -1092,11 +1098,11 @@ export const DocumentsWithMemoriesResponseSchema = z export const DocumentsWithMemoriesQuerySchema = z .object({ - page: z.number().default(1).openapi({ + page: z.number().int().min(1).default(1).openapi({ description: "Page number to fetch", example: 1, }), - limit: z.number().default(10).openapi({ + limit: z.number().int().min(1).default(10).openapi({ description: "Number of items per page", example: 10, }), From f163c932cfac28ab04103ccef8f3de73f220c99d Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:55:01 +0530 Subject: [PATCH 04/24] fix(web): keep highlights card active index in range on refresh (#1334) Co-authored-by: Vedant Mahajan --- apps/web/components/highlights-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/components/highlights-card.tsx b/apps/web/components/highlights-card.tsx index 38257d2e..6caaadf7 100644 --- a/apps/web/components/highlights-card.tsx +++ b/apps/web/components/highlights-card.tsx @@ -92,8 +92,8 @@ export function HighlightsCard({ if (isReplyOpen) replyInputRef.current?.focus() }, [isReplyOpen]) - // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally re-run when items changes useEffect(() => { + setActiveIndex((i) => Math.min(i, Math.max(items.length - 1, 0))) setIsReplyOpen(false) setReplyText("") setIsExpanded(false) From 74b2201eebe9ff44f37bc985682888dceb509cd2 Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:55:53 +0530 Subject: [PATCH 05/24] fix(memory-graph): stop painting expired memories as expiring (#1335) Co-authored-by: Vedant Mahajan --- .../memory-graph/src/__tests__/graph-data-utils.test.ts | 7 +++++++ packages/memory-graph/src/hooks/use-graph-data.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/memory-graph/src/__tests__/graph-data-utils.test.ts b/packages/memory-graph/src/__tests__/graph-data-utils.test.ts index 4f728f97..f98df074 100644 --- a/packages/memory-graph/src/__tests__/graph-data-utils.test.ts +++ b/packages/memory-graph/src/__tests__/graph-data-utils.test.ts @@ -65,6 +65,13 @@ describe("getMemoryBorderColor", () => { expect(getMemoryBorderColor(mem, colors)).toBe(colors.memBorderExpiring) }) + it("does not treat an already-elapsed forgetAfter as expiring", () => { + const past = new Date(Date.now() - 60 * 1000).toISOString() + const old = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() + const mem = makeMemory({ forgetAfter: past, createdAt: old }) + expect(getMemoryBorderColor(mem, colors)).toBe(colors.memStrokeDefault) + }) + it("returns recent color for memories created within 24 hours", () => { const recent = new Date(Date.now() - 1000).toISOString() const mem = makeMemory({ createdAt: recent }) diff --git a/packages/memory-graph/src/hooks/use-graph-data.ts b/packages/memory-graph/src/hooks/use-graph-data.ts index 00a05d10..aed2d6f0 100644 --- a/packages/memory-graph/src/hooks/use-graph-data.ts +++ b/packages/memory-graph/src/hooks/use-graph-data.ts @@ -47,7 +47,7 @@ export function getMemoryBorderColor( if (mem.isForgotten) return colors.memBorderForgotten if (mem.forgetAfter) { const msLeft = new Date(mem.forgetAfter).getTime() - Date.now() - if (msLeft < SEVEN_DAYS_MS) return colors.memBorderExpiring + if (msLeft > 0 && msLeft < SEVEN_DAYS_MS) return colors.memBorderExpiring } const age = Date.now() - new Date(mem.createdAt).getTime() if (age < ONE_DAY_MS) return colors.memBorderRecent From 14bcc92c31b86dc9b8b74c1aadacd45fc6308d6b Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:56:23 +0530 Subject: [PATCH 06/24] fix(memory-graph): center arrow-key navigation in the visible graph area (#1337) --- packages/memory-graph/src/components/memory-graph.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index 9d0c2ef6..d165ac71 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -467,10 +467,10 @@ export function MemoryGraph({ n.x, n.y, containerSize.width, - containerSize.height, + graphFitHeight, ) }, - [nodes, containerSize.width, containerSize.height], + [nodes, containerSize.width, graphFitHeight], ) const navigateUp = useCallback(() => { From 47152afc1d512d4247b31bdda05ea4efc19f6c96 Mon Sep 17 00:00:00 2001 From: pawan Date: Wed, 12 Aug 2026 20:56:54 +0530 Subject: [PATCH 07/24] fix(openai-sdk): cap supermemory to <3.5 so a fresh install imports (#1236) --- packages/openai-sdk-python/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openai-sdk-python/pyproject.toml b/packages/openai-sdk-python/pyproject.toml index 557e38b0..4fcdf981 100644 --- a/packages/openai-sdk-python/pyproject.toml +++ b/packages/openai-sdk-python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "supermemory-openai-sdk" -version = "1.0.4" +version = "1.0.5" description = "Memory tools for OpenAI function calling with supermemory" readme = "README.md" license = "MIT" @@ -26,7 +26,7 @@ classifiers = [ requires-python = ">=3.8.1" dependencies = [ "openai>=1.102.0", - "supermemory>=3.1.0", + "supermemory>=3.1.0,<3.5.0", "typing-extensions>=4.0.0", "requests>=2.25.0", ] From 00e57fb9c219f353654b65017288de78d783b9ec Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:57:33 +0530 Subject: [PATCH 08/24] fix(web): stop formatUsageNumber rendering 1000.0K at unit boundaries (#1340) Co-authored-by: Vedant Mahajan --- apps/web/lib/billing-utils.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/web/lib/billing-utils.ts b/apps/web/lib/billing-utils.ts index 3360e9c4..c2f7cfaa 100644 --- a/apps/web/lib/billing-utils.ts +++ b/apps/web/lib/billing-utils.ts @@ -185,18 +185,26 @@ export function getBrainTrialInfo( } /** - * Format a number with K/M suffix for display + * Format a number with K/M/B suffix for display * @example formatUsageNumber(1500000) => "1.5M" * @example formatUsageNumber(50000) => "50K" + * @example formatUsageNumber(999950) => "1.0M" */ export function formatUsageNumber(value: number): string { + const withSuffix = (n: number, suffix: string) => + n % 1 === 0 ? `${n}${suffix}` : `${n.toFixed(1)}${suffix}` + if (value >= 1_000_000) { const millions = value / 1_000_000 - return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M` + return millions >= 999.95 + ? withSuffix(value / 1_000_000_000, "B") + : withSuffix(millions, "M") } if (value >= 1_000) { const thousands = value / 1_000 - return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K` + return thousands >= 999.95 + ? withSuffix(value / 1_000_000, "M") + : withSuffix(thousands, "K") } return value.toString() } From b7a6ea9a5f1f287a2e323b6125769b6aadf28959 Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Wed, 12 Aug 2026 20:58:06 +0530 Subject: [PATCH 09/24] fix(extension): stop fragmenting Included Memories that contain commas or newlines (#1339) Co-authored-by: Vedant Mahajan --- .../entrypoints/content/chatgpt.ts | 5 ++- .../entrypoints/content/claude.ts | 5 ++- .../entrypoints/content/gemini.ts | 5 ++- .../entrypoints/content/memory-suggestion.ts | 38 +++++++++++++++++-- .../entrypoints/content/t3.ts | 28 ++++++++------ 5 files changed, 63 insertions(+), 18 deletions(-) diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts index 444e3ac8..cf7a3002 100644 --- a/apps/browser-extension/entrypoints/content/chatgpt.ts +++ b/apps/browser-extension/entrypoints/content/chatgpt.ts @@ -17,6 +17,7 @@ import { acceptMemorySuggestion, clearMemorySuggestion, hasAcceptedSupermemoryContext, + serializeMemoriesForDataset, setMemoryMarkerStatus, showLoadingSuggestion, showMarkerPopover, @@ -212,7 +213,9 @@ async function getRelatedMemoriesForChatGPT(actionSource: string) { memoryLength: memoryText.length, }) - iconElement.dataset.memoriesData = String(response.data) + iconElement.dataset.memoriesData = serializeMemoriesForDataset( + response.data, + ) if (isAutoSearch) { setMemoryMarkerStatus(iconElement, "found") diff --git a/apps/browser-extension/entrypoints/content/claude.ts b/apps/browser-extension/entrypoints/content/claude.ts index 7bff4dfc..f31c2bb6 100644 --- a/apps/browser-extension/entrypoints/content/claude.ts +++ b/apps/browser-extension/entrypoints/content/claude.ts @@ -17,6 +17,7 @@ import { acceptMemorySuggestion, clearMemorySuggestion, hasAcceptedSupermemoryContext, + serializeMemoriesForDataset, setMemoryMarkerStatus, showLoadingSuggestion, showMarkerPopover, @@ -459,7 +460,9 @@ async function getRelatedMemoriesForClaude(actionSource: string) { memoryLength: memoryText.length, }) - iconElement.dataset.memoriesData = String(response.data) + iconElement.dataset.memoriesData = serializeMemoriesForDataset( + response.data, + ) if (isAutoSearch) { setMemoryMarkerStatus(iconElement, "found") diff --git a/apps/browser-extension/entrypoints/content/gemini.ts b/apps/browser-extension/entrypoints/content/gemini.ts index 6ece78df..f819d3d6 100644 --- a/apps/browser-extension/entrypoints/content/gemini.ts +++ b/apps/browser-extension/entrypoints/content/gemini.ts @@ -17,6 +17,7 @@ import { acceptMemorySuggestion, clearMemorySuggestion, hasAcceptedSupermemoryContext, + serializeMemoriesForDataset, setMemoryMarkerStatus, showLoadingSuggestion, showMarkerPopover, @@ -417,7 +418,9 @@ async function getRelatedMemoriesForGemini(actionSource: string) { if (response?.success && response?.data && input) { const memoryText = showMemorySuggestion("gemini", input, response.data) - iconElement.dataset.memoriesData = String(response.data) + iconElement.dataset.memoriesData = serializeMemoriesForDataset( + response.data, + ) iconElement.dataset.supermemories = memoryText if (isAutoSearch) { setMemoryMarkerStatus(iconElement, "found") diff --git a/apps/browser-extension/entrypoints/content/memory-suggestion.ts b/apps/browser-extension/entrypoints/content/memory-suggestion.ts index 1722e71e..18861fe3 100644 --- a/apps/browser-extension/entrypoints/content/memory-suggestion.ts +++ b/apps/browser-extension/entrypoints/content/memory-suggestion.ts @@ -12,6 +12,39 @@ export function buildSupermemoryText(memories: unknown): string { return `\n\n${SUPERMEMORY_PREFIX} ${memoryText}` } +function normalizeMemoryList(memories: unknown): string[] { + const list = Array.isArray(memories) + ? memories + : memories == null + ? [] + : [memories] + return list + .map((memory) => (typeof memory === "string" ? memory : String(memory))) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0) +} + +export function serializeMemoriesForDataset(memories: unknown): string { + const list = normalizeMemoryList(memories) + return list.length > 0 ? JSON.stringify(list) : "" +} + +export function parseMemoriesFromDataset( + raw: string | null | undefined, +): string[] { + if (!raw) return [] + try { + const parsed = JSON.parse(raw) + if (Array.isArray(parsed)) return normalizeMemoryList(parsed) + } catch { + // Not JSON — fall through to the legacy delimiter split. + } + return raw + .split(/[,\n]/) + .map((memory) => memory.trim()) + .filter((memory) => memory.length > 0 && memory !== ",") +} + export function showMemorySuggestion( platform: string, input: SuggestionInput, @@ -305,10 +338,7 @@ export function showMarkerPopover( color: rgba(255, 255, 255, 0.76); ` - memories - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") + parseMemoriesFromDataset(memories) .slice(0, 5) .forEach((memory) => { const item = document.createElement("div") diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts index 66a11235..39a05f03 100644 --- a/apps/browser-extension/entrypoints/content/t3.ts +++ b/apps/browser-extension/entrypoints/content/t3.ts @@ -10,6 +10,10 @@ import { autoCapturePromptsEnabled, } from "../../utils/storage" import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components" +import { + parseMemoriesFromDataset, + serializeMemoriesForDataset, +} from "./memory-suggestion" let t3DebounceTimeout: NodeJS.Timeout | null = null let t3RouteObserver: MutationObserver | null = null @@ -233,7 +237,9 @@ async function getRelatedMemoriesForT3(actionSource: string) { if (textareaElement) { textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` - iconElement.dataset.memoriesData = response.data + iconElement.dataset.memoriesData = serializeMemoriesForDataset( + response.data, + ) updateT3IconFeedback("Included Memories", iconElement) } else { @@ -329,11 +335,9 @@ function updateT3IconFeedback( overflow-y: auto; ` - const memoriesText = iconElement.dataset.memoriesData || "" - const individualMemories = memoriesText - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") + const individualMemories = parseMemoriesFromDataset( + iconElement.dataset.memoriesData, + ) individualMemories.forEach((memory, index) => { const memoryItem = document.createElement("div") @@ -421,15 +425,17 @@ function updateT3IconFeedback( content.removeChild(memoryItem) } - const currentMemories = (iconElement.dataset.memoriesData || "") - .split(/[,\n]/) - .map((memory) => memory.trim()) - .filter((memory) => memory.length > 0 && memory !== ",") + const currentMemories = parseMemoriesFromDataset( + iconElement.dataset.memoriesData, + ) currentMemories.splice(index, 1) + // Injected prompt keeps its existing joined-text form; the popup's + // own data is stored as JSON so comma-bearing memories stay intact. const updatedMemories = currentMemories.join(" ,") - iconElement.dataset.memoriesData = updatedMemories + iconElement.dataset.memoriesData = + serializeMemoriesForDataset(currentMemories) const textareaElement = (document.querySelector("textarea") as HTMLTextAreaElement) || From c70c142fc78bb748010489ec37f3b06994ee93e3 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Wed, 12 Aug 2026 13:12:54 -0700 Subject: [PATCH 10/24] fix(web): open Slack install in new window (#1460) --- apps/web/components/onboarding-brain/research-action-rail.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/components/onboarding-brain/research-action-rail.tsx b/apps/web/components/onboarding-brain/research-action-rail.tsx index ce79d015..c951f94f 100644 --- a/apps/web/components/onboarding-brain/research-action-rail.tsx +++ b/apps/web/components/onboarding-brain/research-action-rail.tsx @@ -525,6 +525,8 @@ function SlackStepBody({
Date: Thu, 13 Aug 2026 06:58:47 +0000 Subject: [PATCH 11/24] feat(web): take a card before the Company Brain trial starts (#1459) Onboarding now opens a trial step that collects a card through Stripe checkout before the brain is enabled, with a timeline showing today's $0, the day-12 reminder, and the day-14 charge. - Only leaves the card step once the API confirms the trial is live - Brain home shows a setup banner and dims what the trial unlocks - Recovers orgs that abandoned checkout instead of stranding them - Adds the organization ID to account settings, copyable from the label --- .../components/brain-home/brain-home-view.tsx | 21 +- .../brain-home/connections-board.tsx | 47 +++- apps/web/components/dashboard-view.tsx | 2 + .../company-brain-onboarding.tsx | 51 +++- .../onboarding-brain/research-action-rail.tsx | 2 +- .../onboarding-brain/step-trial.tsx | 239 ++++++++++++++++++ apps/web/components/settings/account.tsx | 19 +- apps/web/components/slack-connect-card.tsx | 24 +- apps/web/components/trial-setup-banner.tsx | 41 +++ apps/web/hooks/use-trial-status.ts | 34 +++ apps/web/lib/analytics.ts | 5 + 11 files changed, 451 insertions(+), 34 deletions(-) create mode 100644 apps/web/components/onboarding-brain/step-trial.tsx create mode 100644 apps/web/components/trial-setup-banner.tsx create mode 100644 apps/web/hooks/use-trial-status.ts diff --git a/apps/web/components/brain-home/brain-home-view.tsx b/apps/web/components/brain-home/brain-home-view.tsx index 06a00ba6..3501c5e8 100644 --- a/apps/web/components/brain-home/brain-home-view.tsx +++ b/apps/web/components/brain-home/brain-home-view.tsx @@ -8,6 +8,8 @@ import { ArrowRight, Check, FileText, Loader2, UserPlus } from "lucide-react" import { useQueryState } from "nuqs" import { useSettingsModal } from "@/components/settings/settings-modal" import { useBrainTrial } from "@/hooks/use-brain-trial" +import { TrialSetupBanner } from "@/components/trial-setup-banner" +import { useTrialStatus } from "@/hooks/use-trial-status" import { dmSans125ClassName } from "@/lib/fonts" import { useViewMode } from "@/lib/view-mode-context" import { @@ -170,6 +172,7 @@ export function BrainHomeView() { const o = useBrainOverview() const trial = useBrainTrial() const board = useConnectionsBoard() + const { needsSetup } = useTrialStatus() // Rows with no reported state (older orgs, pre-Slack) don't count or render. const milestones = [ ...(o.researchStatus != null ? [o.researchStatus === "done"] : []), @@ -186,6 +189,7 @@ export function BrainHomeView() { return (
+ - {board.slack && !board.slack.connected && } + {board.slack && !board.slack.connected && !needsSetup && }
{board.showBoard && } @@ -486,7 +490,7 @@ function BrainTimeline({ canInvite: boolean toolsCardVisible: boolean }) { - const trial = useBrainTrial() + const { needsSetup } = useTrialStatus() const { openSettings } = useSettingsModal() const { setViewMode } = useViewMode() const [, setInvite] = useQueryState("invite") @@ -532,12 +536,13 @@ function BrainTimeline({ title: slackConnected ? "Slack connected" : "Connect Slack", hint: slackConnected ? undefined - : trial.state === "trialing" - ? "Ask your brain from any channel." - : "Starts your 14-day free trial. No credit card needed.", - action: slackConnected - ? undefined - : { label: "Add", href: `${BACKEND}/brain/slack/oauth/install` }, + : needsSetup + ? "Starts with your trial." + : "Ask your brain from any channel.", + action: + slackConnected || needsSetup + ? undefined + : { label: "Add", href: `${BACKEND}/brain/slack/oauth/install` }, }, ...(rollout != null ? [ diff --git a/apps/web/components/brain-home/connections-board.tsx b/apps/web/components/brain-home/connections-board.tsx index 04ab056c..3c79256e 100644 --- a/apps/web/components/brain-home/connections-board.tsx +++ b/apps/web/components/brain-home/connections-board.tsx @@ -4,6 +4,7 @@ import { cn } from "@lib/utils" import { ArrowRight, Loader2 } from "lucide-react" import { useCallback, useEffect, useState } from "react" import { toast } from "sonner" +import { useTrialStatus } from "@/hooks/use-trial-status" import { dmSans125ClassName } from "@/lib/fonts" import { useViewMode } from "@/lib/view-mode-context" import { brainConnectorIcon, SlackMark } from "../brain-connector-icons" @@ -192,6 +193,7 @@ export const CONNECT_TOOLS_CARD_ID = "connect-tools" export function ConnectToolsCard({ board }: { board: ConnectionsBoardState }) { const { setViewMode } = useViewMode() const { loading, featured, overflow, busy, isConnected, connect } = board + const { needsSetup } = useTrialStatus() return (

- Give your Slack agent live access to the apps your team already uses. + {needsSetup + ? "Starts with your trial." + : "Give your Slack agent live access to the apps your team already uses."}

-
+
{loading ? ( Array.from({ length: 3 }).map((_, i) => ( @@ -248,6 +258,7 @@ export function ConnectToolsCard({ board }: { board: ConnectionsBoardState }) { export function AskInSlackCard({ board }: { board: ConnectionsBoardState }) { const { previewApps, isConnected, connectedCount } = board + const { needsSetup } = useTrialStatus() const prompts = previewApps .filter((a) => AGENT_PROMPTS[a.slug]) .slice(0, 6) @@ -275,12 +286,19 @@ export function AskInSlackCard({ board }: { board: ConnectionsBoardState }) {

- {connectedCount > 0 - ? "Things your agent can answer now:" - : "Connect a tool and your agent can answer:"} + {needsSetup + ? "Starts with your trial." + : connectedCount > 0 + ? "Things your agent can answer now:" + : "Connect a tool and your agent can answer:"}

-
+
{prompts.map((p, i) => ( diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx index 93559ace..12c76a86 100644 --- a/apps/web/components/dashboard-view.tsx +++ b/apps/web/components/dashboard-view.tsx @@ -32,6 +32,7 @@ import { StaticGraphPreview } from "@/components/memory-graph/graph-card" import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip" import { ChromeIcon, RaycastIcon } from "@/components/integration-icons" import { SlackConnectCard } from "@/components/slack-connect-card" +import { TrialSetupBanner } from "@/components/trial-setup-banner" import { GoogleDrive, Notion, MCPIcon } from "@ui/assets/icons" import { analytics } from "@/lib/analytics" import type { IntegrationParamValue } from "@/lib/search-params" @@ -1344,6 +1345,7 @@ export function DashboardView({ )} >
+ {headerNotice ?
{headerNotice}
: null} diff --git a/apps/web/components/onboarding-brain/company-brain-onboarding.tsx b/apps/web/components/onboarding-brain/company-brain-onboarding.tsx index e9a0673a..3d69acb7 100644 --- a/apps/web/components/onboarding-brain/company-brain-onboarding.tsx +++ b/apps/web/components/onboarding-brain/company-brain-onboarding.tsx @@ -30,6 +30,9 @@ import { UserAvatar, } from "./step-about" import { ResearchActionRail } from "./research-action-rail" +import { CHECKOUT_RETURN_PARAM, StepTrial } from "./step-trial" +import { useTrialStatus } from "@/hooks/use-trial-status" +import { analytics } from "@/lib/analytics" import { type CompanyBrainConfirmResult, type CompanyBrainOrganizationChoice, @@ -52,7 +55,7 @@ interface CompanyBrainOnboardingProps { const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" -type Phase = "confirm" | "research" +type Phase = "confirm" | "trial" | "research" function normalizeDomain(input: string): string { const host = input @@ -82,6 +85,23 @@ export function CompanyBrainOnboarding({ onUsePersonal, }: CompanyBrainOnboardingProps) { const [phase, setPhase] = useState("confirm") + const { needsSetup } = useTrialStatus() + const resumedRef = useRef(false) + useEffect(() => { + if (resumedRef.current) return + const url = new URL(window.location.href) + if (url.searchParams.get(CHECKOUT_RETURN_PARAM) !== "complete") return + resumedRef.current = true + url.searchParams.delete(CHECKOUT_RETURN_PARAM) + window.history.replaceState({}, "", `${url.pathname}${url.search}`) + setPhase("research") + }, []) + useEffect(() => { + if (resumedRef.current || !needsSetup || phase !== "confirm") return + resumedRef.current = true + setPhase("trial") + analytics.brainTrialCardViewed() + }, [needsSetup, phase]) const [domain, setDomain] = useState(initialDomain) const [organizationChoices, setOrganizationChoices] = useState< CompanyBrainOrganizationChoice[] | null @@ -107,7 +127,8 @@ export function CompanyBrainOnboarding({ } setOrganizationChoices(null) setServerSchedulesResearch(result.serverSchedulesResearch) - setPhase("research") + setPhase("trial") + analytics.brainTrialCardViewed() } // New-org signup schedules research after provisioning; if that hook is slow @@ -203,9 +224,9 @@ export function CompanyBrainOnboarding({
{/* Persistent card: full confirm card, then morphs into a slim docked header. */} @@ -215,13 +236,25 @@ export function CompanyBrainOnboarding({ style={cardSurfaceStyle} className={cn( "w-full mx-auto rounded-[22px] bg-[#1B1F24]", - phase === "confirm" - ? "max-w-xl p-6 md:p-8" - : "max-w-7xl px-5 py-3 xl:max-w-[1360px]", + phase === "research" + ? "max-w-7xl px-5 py-3 xl:max-w-[1360px]" + : phase === "trial" + ? "max-w-4xl p-6 md:p-7" + : "max-w-xl p-6 md:p-8", )} > - {phase === "confirm" ? ( + {phase === "trial" ? ( + + setPhase("research")} /> + + ) : phase === "confirm" ? (

- Starts your 14-day free trial. No credit card needed. + Included in your 14-day trial.

) diff --git a/apps/web/components/onboarding-brain/step-trial.tsx b/apps/web/components/onboarding-brain/step-trial.tsx new file mode 100644 index 00000000..48bc1823 --- /dev/null +++ b/apps/web/components/onboarding-brain/step-trial.tsx @@ -0,0 +1,239 @@ +"use client" + +import { Gmail, GoogleDrive, Granola, MCPIcon, Notion } from "@ui/assets/icons" +import { GradientLogo } from "@ui/assets/Logo" +import { Button } from "@ui/components/button" +import { cn } from "@lib/utils" +import { ArrowRight, Loader2, ShieldCheck } from "lucide-react" +import { useState } from "react" +import { toast } from "sonner" +import { SlackMark } from "@/components/brain-connector-icons" +import { analytics } from "@/lib/analytics" +import { dmSans125ClassName } from "@/lib/fonts" + +const BACKEND = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + +export const CHECKOUT_RETURN_PARAM = "brainTrial" + +const TRIAL_DAYS = 14 +/** The only reminder that lands before the charge; 15 and 17 are post-trial. */ +const REMINDER_DAY = 12 +const MONTHLY_PRICE = "$100" + +function checkoutReturnUrl(): string { + const url = new URL(window.location.href) + url.searchParams.set(CHECKOUT_RETURN_PARAM, "complete") + return url.toString() +} + +function dayOffset(days: number): string { + const at = new Date(Date.now() + days * 24 * 60 * 60 * 1000) + return at.toLocaleDateString(undefined, { month: "short", day: "numeric" }) +} + +const ORBIT = [ + { key: "slack", r: 74, deg: 0, node: }, + { key: "gmail", r: 74, deg: 128, node: }, + { key: "notion", r: 74, deg: 236, node: }, + { key: "drive", r: 112, deg: 58, node: }, + { key: "granola", r: 112, deg: 172, node: }, + { key: "mcp", r: 112, deg: 296, node: }, +] + +const SPIN = "motion-safe:animate-[spin_44s_linear_infinite]" +const SPIN_BACK = "motion-safe:animate-[spin_44s_linear_infinite_reverse]" + +function BrainPanel() { + return ( +
+
+ ) +} + +function TimelineRow({ + date, + title, + value, + current, +}: { + date: string + title: string + value?: string + current?: boolean +}) { + return ( +
  • +
  • + ) +} + +export function StepTrial({ onActive }: { onActive: () => void }) { + const [starting, setStarting] = useState(false) + + const start = async () => { + if (starting) return + setStarting(true) + analytics.brainTrialCheckoutStarted() + try { + const res = await fetch(`${BACKEND}/brain/trial/start`, { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ successUrl: checkoutReturnUrl() }), + }) + const data = (await res.json()) as { + checkoutUrl?: string | null + status?: string + error?: string + } + if (res.status === 409 || data.error === "trial_unavailable") { + throw new Error( + "This workspace has already used its free trial. Upgrade from billing to continue.", + ) + } + if (!res.ok) throw new Error(data.error ?? "Couldn't start the trial.") + if (data.checkoutUrl) { + window.location.href = data.checkoutUrl + return + } + if (data.status === "already_active" || data.status === "attached") { + onActive() + return + } + throw new Error("Couldn't start the trial.") + } catch (error) { + console.error("Failed to start trial:", error) + toast.error( + error instanceof Error ? error.message : "Couldn't start the trial.", + ) + setStarting(false) + } + } + + return ( +
    +
    +
    +

    + Start your {TRIAL_DAYS}-day trial +

    +

    + We take a card now so Company Brain keeps working when the trial + ends. Cancel any time before then and you won't be charged. +

    +
    + +
      +
    + +
    + +

    + + Secured by Stripe · Cancel in one click +

    +
    +
    + + +
    + ) +} diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 99e9bf47..41d74634 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -23,6 +23,7 @@ import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" import * as DialogPrimitive from "@radix-ui/react-dialog" import { useMutation, useQuery } from "@tanstack/react-query" import { + Copy, LoaderIcon, ChevronDown, Users, @@ -458,10 +459,26 @@ export default function Account({ Organization + {org?.id ? ( + + ) : null} {isEditingOrgName ? (
    (null) + const [trialActive, setTrialActive] = useState(true) const [loading, setLoading] = useState(true) useEffect(() => { @@ -58,10 +59,16 @@ export function SlackConnectCard() { let active = true ;(async () => { try { - const res = await fetch(`${BACKEND}/brain/slack/status`, { - credentials: "include", - }) - if (active && res.ok) setStatus((await res.json()) as SlackStatus) + const [slackRes, trialRes] = await Promise.all([ + fetch(`${BACKEND}/brain/slack/status`, { credentials: "include" }), + fetch(`${BACKEND}/brain/trial/status`, { credentials: "include" }), + ]) + if (!active) return + if (slackRes.ok) setStatus((await slackRes.json()) as SlackStatus) + if (trialRes.ok) { + const trial = (await trialRes.json()) as { active?: boolean } + setTrialActive(Boolean(trial.active)) + } } finally { if (active) setLoading(false) } @@ -92,7 +99,7 @@ export function SlackConnectCard() { Connected - ) : ( + ) : trialActive ? ( Add to Slack + ) : ( + + Finish setting up + )}
    ) diff --git a/apps/web/components/trial-setup-banner.tsx b/apps/web/components/trial-setup-banner.tsx new file mode 100644 index 00000000..b96e5c38 --- /dev/null +++ b/apps/web/components/trial-setup-banner.tsx @@ -0,0 +1,41 @@ +"use client" + +import { ArrowRight, CreditCard } from "lucide-react" +import Link from "next/link" +import { useTrialStatus } from "@/hooks/use-trial-status" + +export function TrialSetupBanner() { + const { needsSetup, data } = useTrialStatus() + if (!needsSetup) return null + + const endedTrial = data?.reason === "trial_ended" + + return ( +
    +
    + + + +
    +

    + {endedTrial + ? "Your Company Brain trial has ended" + : "Finish setting up Company Brain"} +

    +

    + {endedTrial + ? "Move to Max or Scale to switch the brain back on." + : "Add a card to start your 14-day trial. $0 today."} +

    +
    +
    + + {endedTrial ? "Upgrade" : "Add card"} + + +
    + ) +} diff --git a/apps/web/hooks/use-trial-status.ts b/apps/web/hooks/use-trial-status.ts new file mode 100644 index 00000000..78941199 --- /dev/null +++ b/apps/web/hooks/use-trial-status.ts @@ -0,0 +1,34 @@ +import { useQuery } from "@tanstack/react-query" +import { useHasCompanyBrain } from "@/hooks/use-company-brain" + +const BACKEND = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + +export type TrialStatus = { + active: boolean + reason: string | null +} + +/** Distinguishes a named Company Brain org from one whose trial is actually live. */ +export function useTrialStatus() { + const isCompanyBrain = useHasCompanyBrain() + + const query = useQuery({ + queryKey: ["brain", "trial-status"], + queryFn: async (): Promise => { + const res = await fetch(`${BACKEND}/brain/trial/status`, { + credentials: "include", + }) + if (!res.ok) throw new Error("Failed to load trial status") + const data = (await res.json()) as { active?: boolean; reason?: string } + return { active: Boolean(data.active), reason: data.reason ?? null } + }, + enabled: isCompanyBrain, + staleTime: 30 * 1000, + }) + + return { + ...query, + needsSetup: isCompanyBrain && query.data ? !query.data.active : false, + } +} diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index 5ffda827..da1264f1 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -271,4 +271,9 @@ export const analytics = { }) => safeCapture("company_brain_promo_clicked", props), companyBrainPromoDismissed: () => safeCapture("company_brain_promo_dismissed"), + + brainTrialCardViewed: () => safeCapture("brain_trial_card_viewed"), + brainTrialCheckoutStarted: () => safeCapture("brain_trial_checkout_started"), + brainTrialCheckoutAbandoned: () => + safeCapture("brain_trial_checkout_abandoned"), } From fcf49855cef2e6da138898bd042af76f800ac41a Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Thu, 13 Aug 2026 17:08:50 +0530 Subject: [PATCH 12/24] Upgrade Nova model picker to current runtime models (#1404) --- .../components/chat/home-chat-composer.tsx | 4 ++-- apps/web/components/chat/index.tsx | 4 ++-- apps/web/components/chat/model-selector.tsx | 3 +-- apps/web/lib/chat-stream-error.ts | 6 ++--- apps/web/lib/models.tsx | 24 +++++++++---------- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/apps/web/components/chat/home-chat-composer.tsx b/apps/web/components/chat/home-chat-composer.tsx index 7f9d28af..d3328d73 100644 --- a/apps/web/components/chat/home-chat-composer.tsx +++ b/apps/web/components/chat/home-chat-composer.tsx @@ -36,9 +36,9 @@ export function HomeChatComposer({ const [attachmentDrafts, setAttachmentDrafts] = useState< ChatAttachmentDraft[] >([]) - const [selectedModel, setSelectedModel] = useState("grok-4.3") + const [selectedModel, setSelectedModel] = useState("grok-4.5") const [reasoningEffort, setReasoningEffort] = useState( - getDefaultReasoningEffort("grok-4.3"), + getDefaultReasoningEffort("grok-4.5"), ) const { selectedProject } = useProject() const [chatSpaceProjects, setChatSpaceProjects] = useState([ diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index c78df7f7..70f01d06 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -206,11 +206,11 @@ export function ChatSidebar({ >([]) const [isChatDraggingFiles, setIsChatDraggingFiles] = useState(false) const [selectedModel, setSelectedModel] = useState( - initialSelectedModel ?? "grok-4.3", + initialSelectedModel ?? "grok-4.5", ) const [reasoningEffort, setReasoningEffort] = useState( initialReasoningEffort ?? - getDefaultReasoningEffort(initialSelectedModel ?? "grok-4.3"), + getDefaultReasoningEffort(initialSelectedModel ?? "grok-4.5"), ) const selectedModelRef = useRef(selectedModel) selectedModelRef.current = selectedModel diff --git a/apps/web/components/chat/model-selector.tsx b/apps/web/components/chat/model-selector.tsx index ca9935ee..b65cbad7 100644 --- a/apps/web/components/chat/model-selector.tsx +++ b/apps/web/components/chat/model-selector.tsx @@ -23,8 +23,7 @@ export default function ChatModelSelector({ minimal = false, dropdownDirection = "up", }: ChatModelSelectorProps = {}) { - const [internalModel, setInternalModel] = - useState("claude-sonnet-4.6") + const [internalModel, setInternalModel] = useState("claude-sonnet-5") const [isOpen, setIsOpen] = useState(false) const containerRef = useRef(null) diff --git a/apps/web/lib/chat-stream-error.ts b/apps/web/lib/chat-stream-error.ts index 2e17135d..f31dc53b 100644 --- a/apps/web/lib/chat-stream-error.ts +++ b/apps/web/lib/chat-stream-error.ts @@ -1,9 +1,9 @@ import type { ModelId } from "@/lib/models" const OTHER_MODELS: ModelId[] = [ - "gpt-5.1", - "claude-sonnet-4.6", - "gemini-2.5-pro", + "gpt-5.6-terra", + "claude-sonnet-5", + "gemini-3.1-pro-preview", ] function flattenError(e: unknown): string { diff --git a/apps/web/lib/models.tsx b/apps/web/lib/models.tsx index 5d3479ef..fe0e7584 100644 --- a/apps/web/lib/models.tsx +++ b/apps/web/lib/models.tsx @@ -1,22 +1,22 @@ export const models = [ { - id: "grok-4.3", - name: "Grok 4.3", + id: "grok-4.5", + name: "Grok 4.5", description: "xAI's latest model", }, { - id: "gpt-5.1", - name: "GPT 5.1", + id: "gpt-5.6-terra", + name: "GPT 5.6", description: "OpenAI's latest model", }, { - id: "claude-sonnet-4.6", - name: "Claude Sonnet 4.6", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", description: "Anthropic's advanced model", }, { - id: "gemini-2.5-pro", - name: "Gemini 3 Pro", + id: "gemini-3.1-pro-preview", + name: "Gemini 3.1 Pro", description: "Google's most capable model", }, ] as const @@ -25,10 +25,10 @@ export type ModelId = (typeof models)[number]["id"] export type ReasoningEffort = "instant" | "thinking" export const modelNames: Record = { - "grok-4.3": { name: "Grok", version: "4.3" }, - "gpt-5.1": { name: "GPT", version: "5.1" }, - "claude-sonnet-4.6": { name: "Claude", version: "4.6" }, - "gemini-2.5-pro": { name: "Gemini", version: "3 Pro" }, + "grok-4.5": { name: "Grok", version: "4.5" }, + "gpt-5.6-terra": { name: "GPT", version: "5.6" }, + "claude-sonnet-5": { name: "Claude", version: "Sonnet 5" }, + "gemini-3.1-pro-preview": { name: "Gemini", version: "3.1 Pro" }, } export const reasoningOptions: Array<{ From 1356affbd16d7d88ef6dd223936be36f6fc2cabe Mon Sep 17 00:00:00 2001 From: James Yang Date: Thu, 13 Aug 2026 08:25:16 -0400 Subject: [PATCH 13/24] fix(extension): finish Included Memories leftovers on T3 (#1257) (#1421) Co-authored-by: abhay-codes07 Co-authored-by: Vedant Mahajan --- .../entrypoints/content/memory-suggestion.ts | 7 ++ .../entrypoints/content/t3.ts | 95 +++++++++++-------- 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/apps/browser-extension/entrypoints/content/memory-suggestion.ts b/apps/browser-extension/entrypoints/content/memory-suggestion.ts index 18861fe3..27b65b08 100644 --- a/apps/browser-extension/entrypoints/content/memory-suggestion.ts +++ b/apps/browser-extension/entrypoints/content/memory-suggestion.ts @@ -45,6 +45,13 @@ export function parseMemoriesFromDataset( .filter((memory) => memory.length > 0 && memory !== ",") } +export function renumberIncludedMemories(memories: string[]): string[] { + return memories.map((memory, index) => { + const text = memory.replace(/^\d+\.\s*/, "").replace(/\s+$/, "") + return `${index + 1}. ${text} \n` + }) +} + export function showMemorySuggestion( platform: string, input: SuggestionInput, diff --git a/apps/browser-extension/entrypoints/content/t3.ts b/apps/browser-extension/entrypoints/content/t3.ts index 39a05f03..bddd83ed 100644 --- a/apps/browser-extension/entrypoints/content/t3.ts +++ b/apps/browser-extension/entrypoints/content/t3.ts @@ -11,7 +11,9 @@ import { } from "../../utils/storage" import { createT3InputBarElement, DOMUtils } from "../../utils/ui-components" import { + buildSupermemoryText, parseMemoriesFromDataset, + renumberIncludedMemories, serializeMemoriesForDataset, } from "./memory-suggestion" @@ -19,6 +21,19 @@ let t3DebounceTimeout: NodeJS.Timeout | null = null let t3RouteObserver: MutationObserver | null = null let t3UrlCheckInterval: NodeJS.Timeout | null = null let t3ObserverThrottle: NodeJS.Timeout | null = null +let t3IncludedPopup: { + el: HTMLElement + onClick: (event: MouseEvent) => void + timer: ReturnType +} | null = null + +function disposeT3IncludedPopup() { + if (!t3IncludedPopup) return + document.removeEventListener("click", t3IncludedPopup.onClick) + clearTimeout(t3IncludedPopup.timer) + t3IncludedPopup.el.remove() + t3IncludedPopup = null +} export function initializeT3() { if (!DOMUtils.isOnDomain(DOMAINS.T3)) { @@ -57,6 +72,7 @@ function setupT3RouteChangeDetection() { const checkForRouteChange = () => { if (window.location.href !== currentUrl) { + disposeT3IncludedPopup() currentUrl = window.location.href setTimeout(() => { addSupermemoryIconToT3Input() @@ -235,7 +251,9 @@ async function getRelatedMemoriesForT3(actionSource: string) { } if (textareaElement) { - textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${response.data}` + textareaElement.dataset.supermemories = buildSupermemoryText( + response.data, + ) iconElement.dataset.memoriesData = serializeMemoriesForDataset( response.data, @@ -274,6 +292,8 @@ function updateT3IconFeedback( iconElement.dataset.originalHtml = iconElement.innerHTML } + disposeT3IncludedPopup() + const feedbackDiv = document.createElement("div") feedbackDiv.style.cssText = ` display: flex; @@ -409,68 +429,65 @@ function updateT3IconFeedback( popup.style.display = "block" }) - document.addEventListener("click", (e) => { + const onClick = (e: MouseEvent) => { if (!popup.contains(e.target as Node)) { popup.style.display = "none" } - }) + } + document.addEventListener("click", onClick) + t3IncludedPopup = { + el: popup, + onClick, + timer: setTimeout(disposeT3IncludedPopup, 300000), + } content.querySelectorAll("button[data-memory-index]").forEach((button) => { const htmlButton = button as HTMLButtonElement htmlButton.addEventListener("click", () => { const index = Number.parseInt(htmlButton.dataset.memoryIndex || "0", 10) - const memoryItem = htmlButton.parentElement + htmlButton.parentElement?.remove() - if (memoryItem) { - content.removeChild(memoryItem) - } - - const currentMemories = parseMemoriesFromDataset( + const remainingMemories = parseMemoriesFromDataset( iconElement.dataset.memoriesData, ) - currentMemories.splice(index, 1) - - // Injected prompt keeps its existing joined-text form; the popup's - // own data is stored as JSON so comma-bearing memories stay intact. - const updatedMemories = currentMemories.join(" ,") - - iconElement.dataset.memoriesData = - serializeMemoriesForDataset(currentMemories) + remainingMemories.splice(index, 1) + const remaining = renumberIncludedMemories(remainingMemories) const textareaElement = (document.querySelector("textarea") as HTMLTextAreaElement) || (document.querySelector('div[contenteditable="true"]') as HTMLElement) + + // Only wipe when nothing remains — `<= 1` used to discard the last kept memory. + if (remaining.length === 0) { + if (textareaElement?.dataset.supermemories) { + delete textareaElement.dataset.supermemories + } + delete iconElement.dataset.memoriesData + iconElement.innerHTML = iconElement.dataset.originalHtml || "" + delete iconElement.dataset.originalHtml + disposeT3IncludedPopup() + return + } + + iconElement.dataset.memoriesData = + serializeMemoriesForDataset(remaining) if (textareaElement) { - textareaElement.dataset.supermemories = `\n\nSupermemories of user (only for the reference): ${updatedMemories}` + textareaElement.dataset.supermemories = + buildSupermemoryText(remaining) } content .querySelectorAll("button[data-memory-index]") .forEach((btn, newIndex) => { const htmlBtn = btn as HTMLButtonElement - htmlBtn.dataset.memoryIndex = newIndex.toString() + htmlBtn.dataset.memoryIndex = String(newIndex) + const label = htmlBtn.previousElementSibling + if (label) { + label.textContent = remaining[newIndex].trim() + } }) - - if (currentMemories.length <= 1) { - if (textareaElement?.dataset.supermemories) { - delete textareaElement.dataset.supermemories - delete iconElement.dataset.memoriesData - iconElement.innerHTML = iconElement.dataset.originalHtml || "" - delete iconElement.dataset.originalHtml - } - popup.style.display = "none" - if (document.body.contains(popup)) { - document.body.removeChild(popup) - } - } }) }) - - setTimeout(() => { - if (document.body.contains(popup)) { - document.body.removeChild(popup) - } - }, 300000) } iconElement.innerHTML = "" @@ -562,6 +579,7 @@ function setupT3PromptCapture() { if (textareaElement?.dataset.supermemories) { delete textareaElement.dataset.supermemories } + disposeT3IncludedPopup() } const handleT3SendButtonClick = async (event: Event) => { @@ -717,6 +735,7 @@ async function setupT3AutoFetch() { if (textareaElement.dataset.supermemories) { delete textareaElement.dataset.supermemories } + disposeT3IncludedPopup() } }, UI_CONFIG.AUTO_SEARCH_DEBOUNCE_DELAY) } From 9d64e0f9504d958f79d9fe482ac2e6397ee2f112 Mon Sep 17 00:00:00 2001 From: Abhinav Kumar Singh <152812718+abhinavkr26104@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:55:50 +0530 Subject: [PATCH 14/24] fix: add type checks for TypeScript workspaces (#1447) --- apps/browser-extension/package.json | 1 + apps/memory-graph-playground/package.json | 1 + apps/web/package.json | 1 + packages/hooks/package.json | 5 ++++- packages/lib/package.json | 3 +++ packages/ui/package.json | 3 +++ packages/validation/package.json | 5 ++++- 7 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index f40426a8..43f3a56a 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -9,6 +9,7 @@ "dev:firefox": "wxt -b firefox", "build": "wxt build", "build:firefox": "wxt build -b firefox", + "check-types": "bun run compile", "zip": "wxt zip", "zip:firefox": "wxt zip -b firefox", "compile": "tsc --noEmit", diff --git a/apps/memory-graph-playground/package.json b/apps/memory-graph-playground/package.json index 67e31a0b..debe23c7 100644 --- a/apps/memory-graph-playground/package.json +++ b/apps/memory-graph-playground/package.json @@ -7,6 +7,7 @@ "dev": "portless", "dev:app": "next dev --port ${PORT:-3004}", "build": "next build", + "check-types": "tsc --noEmit", "start": "next start" }, "dependencies": { diff --git a/apps/web/package.json b/apps/web/package.json index cddbcd0a..63c4acee 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,6 +10,7 @@ "dev": "portless", "dev:app": "next dev --port ${PORT:-3000}", "build": "next build", + "check-types": "tsc --noEmit", "start": "next start", "lint": "biome check --write", "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 63a7d0e5..780a171f 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,5 +1,8 @@ { "name": "@repo/hooks", "version": "0.0.0", - "private": true + "private": true, + "scripts": { + "check-types": "tsc --noEmit" + } } diff --git a/packages/lib/package.json b/packages/lib/package.json index 99b9d262..c83e27db 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -3,6 +3,9 @@ "version": "0.0.0", "private": true, "type": "module", + "scripts": { + "check-types": "tsc --noEmit" + }, "exports": { "./*": "./*" }, diff --git a/packages/ui/package.json b/packages/ui/package.json index 2a504670..8234997a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -3,6 +3,9 @@ "version": "0.0.0", "private": true, "type": "module", + "scripts": { + "check-types": "tsc --noEmit" + }, "exports": { "./*": "./*" }, diff --git a/packages/validation/package.json b/packages/validation/package.json index ea9f5fc1..8ed19114 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -2,5 +2,8 @@ "name": "@repo/validation", "version": "0.0.0", "private": true, - "type": "module" + "type": "module", + "scripts": { + "check-types": "tsc --noEmit" + } } From 7f448d55d83a9a5db467d9b3f77f6a72de055fcd Mon Sep 17 00:00:00 2001 From: Sarath Donepudi <68329935+dupenodi@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:56:49 +0530 Subject: [PATCH 15/24] docs: document pinned install for supermemory-server (#1238) Co-authored-by: Vedant Mahajan --- apps/docs/self-hosting/quickstart.mdx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/docs/self-hosting/quickstart.mdx b/apps/docs/self-hosting/quickstart.mdx index cb86ee33..d0b37991 100644 --- a/apps/docs/self-hosting/quickstart.mdx +++ b/apps/docs/self-hosting/quickstart.mdx @@ -27,6 +27,28 @@ bunx supermemory local The installer detects your OS and architecture, downloads the right binary, verifies it, and (when run interactively) prompts you for an LLM API key. Supported platforms: macOS (Apple Silicon & Intel), Linux (x64 & arm64). +### Pin or change versions + +Pass an explicit version to install (or roll back to) a specific release instead of `latest`: + +```bash +curl -fsSL https://supermemory.ai/install | bash -s -- 0.0.3 +``` + + +Before rolling back, back up your [data directory](#where-things-live). The installer replaces the binary, but an older server may not understand data or schema changes made by a newer release. + + +Release tags are `server-v` on [GitHub Releases](https://github.com/supermemoryai/supermemory/releases) (for example [`server-v0.0.3`](https://github.com/supermemoryai/supermemory/releases/tag/server-v0.0.3)). + +To move to the newest release later: + +```bash +supermemory-server upgrade +``` + +The binary may also print an “update available” notification on startup. If you intentionally pinned an older version (for example while debugging a regression), you can ignore that message until you are ready to upgrade. + ## Run ```bash From 82dae50ef458139823b3bfd3ebaaaac90ffd8a7c Mon Sep 17 00:00:00 2001 From: Abhinav Kumar Singh <152812718+abhinavkr26104@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:11:00 +0530 Subject: [PATCH 16/24] fix(tools): bound memory forget requests (#1451) --- packages/tools/src/shared/forget-memory.ts | 7 +++++++ packages/tools/src/tool-operations.test.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/tools/src/shared/forget-memory.ts b/packages/tools/src/shared/forget-memory.ts index 50a3a529..8691c92a 100644 --- a/packages/tools/src/shared/forget-memory.ts +++ b/packages/tools/src/shared/forget-memory.ts @@ -1,4 +1,5 @@ const DEFAULT_BASE_URL = "https://api.supermemory.ai" +const FETCH_TIMEOUT_MS = 30_000 export interface ForgetMemoryParams { containerTag: string @@ -7,6 +8,10 @@ export interface ForgetMemoryParams { reason?: string } +export interface ForgetMemoryRequestOptions { + signal?: AbortSignal +} + /** * Marks a memory as forgotten via `DELETE /v4/memories`. * @@ -19,6 +24,7 @@ export async function forgetMemoryRequest( apiKey: string, params: ForgetMemoryParams, baseUrl: string = DEFAULT_BASE_URL, + options?: ForgetMemoryRequestOptions, ): Promise { const response = await fetch(`${baseUrl}/v4/memories`, { method: "DELETE", @@ -27,6 +33,7 @@ export async function forgetMemoryRequest( Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify(params), + signal: options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS), }) if (!response.ok) { diff --git a/packages/tools/src/tool-operations.test.ts b/packages/tools/src/tool-operations.test.ts index 69f12594..136a19be 100644 --- a/packages/tools/src/tool-operations.test.ts +++ b/packages/tools/src/tool-operations.test.ts @@ -109,6 +109,22 @@ describe("memoryForget", () => { id: "mem_1", reason: "outdated", }) + expect(init.signal).toBeInstanceOf(AbortSignal) + }) + + it("uses a caller-provided signal instead of creating a timeout", async () => { + const fetchMock = stubFetch() + const controller = new AbortController() + + await forgetMemoryRequest( + API_KEY, + { containerTag: "user_1", id: "mem_1" }, + undefined, + { signal: controller.signal }, + ) + + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit] + expect(init.signal).toBe(controller.signal) }) it("throws a descriptive error on non-2xx responses", async () => { From eac070048b7066e3d4a29ff4dd17fb9bed8606a3 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Thu, 13 Aug 2026 22:04:01 -0700 Subject: [PATCH 17/24] feat(web): add memory button to company brain navbar (#1468) Co-authored-by: Claude Opus 4.8 --- apps/web/components/company-brain-header.tsx | 42 +++++++++++++++++++- apps/web/components/header.tsx | 7 +++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/apps/web/components/company-brain-header.tsx b/apps/web/components/company-brain-header.tsx index 99cd1116..e7c9258a 100644 --- a/apps/web/components/company-brain-header.tsx +++ b/apps/web/components/company-brain-header.tsx @@ -21,6 +21,7 @@ import { LifeBuoy, LayoutGrid, MenuIcon, + Plus, SearchIcon, Settings, Settings2, @@ -55,6 +56,7 @@ const BACKEND = type SlackStatus = { connected: boolean; teamName: string | null } interface CompanyBrainHeaderProps { + onAddMemory?: () => void onOpenSearch?: () => void } @@ -108,7 +110,10 @@ function useSlackStatus() { }) } -export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) { +export function CompanyBrainHeader({ + onAddMemory, + onOpenSearch, +}: CompanyBrainHeaderProps) { const { user, org, organizations, setActiveOrg } = useAuth() const autumn = useCustomer() const { currentPlan } = useTokenUsage(autumn) @@ -412,6 +417,18 @@ export function CompanyBrainHeader({ onOpenSearch }: CompanyBrainHeaderProps) { "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)", }} > + {onAddMemory && ( + <> + + + Add memory + + + + )} + {onAddMemory && ( + + + + + + Add memory (C) + + + )} {canInvite && ( diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx index c30f84c4..20f76f19 100644 --- a/apps/web/components/header.tsx +++ b/apps/web/components/header.tsx @@ -70,7 +70,12 @@ const brainTileClass = (active: boolean) => export function Header(props: HeaderProps) { const hasCompanyBrain = useHasCompanyBrain() if (hasCompanyBrain) { - return + return ( + + ) } return } From 2e85722cf41e5e3e211e8cc542a852458fed5813 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:36:31 +0000 Subject: [PATCH 18/24] Clarify Company Brain trial copy (#1469) Make the trial terms and payment timing clear, and simplify the call to action. --- apps/web/components/onboarding-brain/step-trial.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/components/onboarding-brain/step-trial.tsx b/apps/web/components/onboarding-brain/step-trial.tsx index 48bc1823..5dd1ce64 100644 --- a/apps/web/components/onboarding-brain/step-trial.tsx +++ b/apps/web/components/onboarding-brain/step-trial.tsx @@ -177,11 +177,11 @@ export function StepTrial({ onActive }: { onActive: () => void }) { "text-[22px] leading-tight font-medium text-[#fafafa]", )} > - Start your {TRIAL_DAYS}-day trial + Start your {TRIAL_DAYS}-day free trial

    - We take a card now so Company Brain keeps working when the trial - ends. Cancel any time before then and you won't be charged. + Add a payment method to start. You will not be charged today. We + will email you before your first payment.

    @@ -221,7 +221,7 @@ export function StepTrial({ onActive }: { onActive: () => void }) { ) : ( <> - Add card and start trial + Start free trial )} From 9cbddcec564c60a8d1b79ffa5b90f7d12335caef Mon Sep 17 00:00:00 2001 From: Prasanna721 <106952318+Prasanna721@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:46:21 +0000 Subject: [PATCH 19/24] docs: historical backfill guide (#1474) Adds a focused guide for backfilling dated documents with `documentDate` and the batch ingestion API. - includes TypeScript and Python batch examples plus optional completion polling - links the guide from the docs navigation and ingestion entry points Validated with `bunx mintlify@latest validate` and `bunx mintlify@latest broken-links`. --- apps/docs/docs.json | 5 + apps/docs/ingestion/add-memories.mdx | 1 + .../batch-ingest-historical-data.mdx | 145 ++++++++++++++++++ apps/docs/using-supermemory.mdx | 1 + 4 files changed, 152 insertions(+) create mode 100644 apps/docs/ingestion/batch-ingest-historical-data.mdx diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 9400ae9f..3891a43b 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -194,6 +194,11 @@ { "group": "Other resources", "pages": [ + { + "group": "General", + "icon": "book-open", + "pages": ["ingestion/batch-ingest-historical-data"] + }, { "group": "Benchmarking", "icon": "flask-conical", diff --git a/apps/docs/ingestion/add-memories.mdx b/apps/docs/ingestion/add-memories.mdx index 39d7c103..ab19a505 100644 --- a/apps/docs/ingestion/add-memories.mdx +++ b/apps/docs/ingestion/add-memories.mdx @@ -496,6 +496,7 @@ console.log(doc.status); // "queued" | "processing" | "done" ## Next Steps +- [How to backfill historical data](/ingestion/batch-ingest-historical-data) — Import dated content with the batch API - [Search Memories](/recall/search) — Query your content - [User Profiles](/recall/user-profiles) — Get user context - [Organizing & Filtering](/concepts/filtering) — Container tags and metadata diff --git a/apps/docs/ingestion/batch-ingest-historical-data.mdx b/apps/docs/ingestion/batch-ingest-historical-data.mdx new file mode 100644 index 00000000..17ff4e1b --- /dev/null +++ b/apps/docs/ingestion/batch-ingest-historical-data.mdx @@ -0,0 +1,145 @@ +--- +title: "How to backfill historical data into Supermemory" +sidebarTitle: "Backfill historical data" +description: "Backfill historical documents into Supermemory with documentDate, stable custom IDs, and the batch ingestion API." +icon: "history" +--- + +Use `POST /v3/documents/batch` to backfill exports, emails, messages, or other dated records. + + + Sort the source data oldest to newest, add `documentDate` to every document. + + +## Backfill in batches + +Backfill dated content by setting `documentDate` on each document, sorting the source records oldest to newest, and sending them in batches. Each request can contain up to 600 documents. + +**Endpoint:** [`POST /v3/documents/batch`](/api-reference/ingest/batch-add-documents) + + + +```typescript TypeScript +import Supermemory from "supermemory"; + +type SourceDocument = { + id: string; + content: string; + createdAt: string; +}; + +const client = new Supermemory(); +const batchSize = 100; + +async function backfillHistoricalData(sourceDocuments: SourceDocument[]) { + const documents = sourceDocuments + .map((document) => ({ + content: document.content, + customId: document.id, + documentDate: new Date(document.createdAt).toISOString() + })) + .sort((a, b) => a.documentDate.localeCompare(b.documentDate)); + + for (let offset = 0; offset < documents.length; offset += batchSize) { + const result = await client.documents.batchAdd({ + containerTag: "historical_import", + documents: documents.slice(offset, offset + batchSize) + }); + + if (result.failed > 0) { + throw new Error(`${result.failed} documents failed to ingest`); + } + } +} +``` + +```python Python +from datetime import datetime, timezone +from supermemory import Supermemory + +client = Supermemory() +batch_size = 100 + +def to_utc(value: str) -> str: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + raise ValueError("created_at must include a timezone") + return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + +def backfill_historical_data(source_documents: list[dict[str, str]]) -> None: + documents = sorted( + [ + { + "content": document["content"], + "custom_id": document["id"], + "document_date": to_utc(document["created_at"]), + } + for document in source_documents + ], + key=lambda document: document["document_date"], + ) + + for offset in range(0, len(documents), batch_size): + result = client.documents.batch_add( + container_tag="historical_import", + documents=documents[offset : offset + batch_size], + ) + + if result.failed > 0: + raise RuntimeError(f"{result.failed} documents failed to ingest") +``` + + + +## Optional: wait for processing to finish + +**Endpoint:** [`GET /v3/documents/{id}`](/api-reference/documents/get-document) + +The batch endpoint returns after accepting the documents. If a later step depends on completed memory generation, poll the returned document IDs until both `status` and `dreamingStatus` are `done`. + + + +```typescript TypeScript +async function waitUntilDone(ids: string[]) { + while (true) { + const documents = await Promise.all( + ids.map((id) => client.documents.get(id)) + ); + + if (documents.some((document) => document.status === "failed")) { + throw new Error("A document failed to process"); + } + + if ( + documents.every( + (document) => + document.status === "done" && document.dreamingStatus === "done" + ) + ) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 10_000)); + } +} +``` + +```python Python +import time + +def wait_until_done(ids: list[str]) -> None: + while True: + documents = [client.documents.get(document_id) for document_id in ids] + + if any(document.status == "failed" for document in documents): + raise RuntimeError("A document failed to process") + + if all( + document.status == "done" and document.dreaming_status == "done" + for document in documents + ): + return + + time.sleep(10) +``` + + diff --git a/apps/docs/using-supermemory.mdx b/apps/docs/using-supermemory.mdx index f65ed2e3..be38bf80 100644 --- a/apps/docs/using-supermemory.mdx +++ b/apps/docs/using-supermemory.mdx @@ -18,6 +18,7 @@ Everything in this section is one of four steps. Same loop whether you're buildi + From 5ecbc263450def05fea29c4adcd30aa85b5af31c Mon Sep 17 00:00:00 2001 From: Dhravya Date: Fri, 14 Aug 2026 22:36:52 +0000 Subject: [PATCH 20/24] fix(mcp): surface real API error messages instead of 'restricted or blocked' (#1406) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why? Plain **T-1554**: a user with a **read-only** MCP OAuth grant got 403s on memory listing, and the client rendered them as *"Access forbidden. Your account may be restricted or blocked."* The API's actual error body said `{"error": "This API key has read-only access"}` — but `handleError` discarded it, so the user (and support) chased a nonexistent account ban. Two masking layers: 1. `handleError` used the raw error `message`, which for our raw-fetch endpoints was a hardcoded string ("Failed to fetch documents") or unparsed JSON, and fell back to the scary "restricted or blocked" text when empty. 2. `getDocuments` didn't read the response body at all. ## What? - New `extractApiErrorMessage()` unwraps JSON error bodies (`{"error": ...}` / `{"message": ...}`) so the API's real reason reaches the user. - `getDocuments` and `listMemoryEntries` now pass the (unwrapped) response body through with the status, letting `handleError` apply status-aware fallbacks when the body is empty. - Reworded the empty-body 403 fallback to point at the common cause first: *"Access forbidden. This connection may be read-only or scoped to specific spaces — reconnect with broader access, or check your account status."* Companion API-side fix (read-only grants couldn't call semantically-read POST list endpoints at all): supermemoryai/mono#2772. ## Testing - Added tests: a 403 with a JSON error body surfaces the API's message; an empty-body 403 gets the scope-aware fallback. `vitest run src/server/client/index.test.ts` — 3 passed. - `tsc --noEmit -p tsconfig.json` clean. (The `check-types` script also runs `tsconfig.widget.json`, which fails on origin/main with a pre-existing `UseAppOptions.strict` error, unrelated.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **Low Risk** > User-facing error text only in the MCP client; no auth or API behavior changes. > > **Overview** > **MCP client errors now show what the API actually returned** instead of hardcoded strings or misleading “restricted or blocked” text. > > Adds `extractApiErrorMessage()` to parse JSON bodies (`error` / `message` fields) from failed responses. **`getDocuments`** and **`listMemoryEntries`** read the response body on non-OK status and attach the unwrapped message (with status) for **`handleError`**, which also uses the helper on error messages. When a 403 has no body message, the fallback now points users toward **read-only or scoped OAuth** rather than an account ban. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1f492470cf4b619e58eea6d45af1dfa0b8cad0c4. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- apps/mcp/src/server/client/index.ts | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/mcp/src/server/client/index.ts b/apps/mcp/src/server/client/index.ts index cc45d438..ad9695a4 100644 --- a/apps/mcp/src/server/client/index.ts +++ b/apps/mcp/src/server/client/index.ts @@ -149,6 +149,19 @@ function objectProperty(value: unknown, key: string): unknown { : undefined } +// API error bodies are JSON like {"error": "..."} — unwrap them so users see +// the real reason instead of raw JSON or a generic fallback. +function extractApiErrorMessage(raw: unknown): string | undefined { + if (typeof raw !== "string" || !raw) return undefined + try { + const parsed = JSON.parse(raw) as { error?: unknown; message?: unknown } + if (typeof parsed.error === "string" && parsed.error) return parsed.error + if (typeof parsed.message === "string" && parsed.message) + return parsed.message + } catch {} + return raw +} + export class SupermemoryClient { private client: Supermemory private containerTag: string @@ -371,7 +384,8 @@ export class SupermemoryClient { signal, }) if (!response.ok) { - throw Object.assign(new Error("Failed to fetch documents"), { + const message = extractApiErrorMessage(await response.text()) + throw Object.assign(new Error(message ?? ""), { status: response.status, }) } @@ -432,11 +446,10 @@ export class SupermemoryClient { }) if (!response.ok) { - const message = await response.text() - throw Object.assign( - new Error(message || "Failed to fetch memory entries"), - { status: response.status }, - ) + const message = extractApiErrorMessage(await response.text()) + throw Object.assign(new Error(message ?? ""), { + status: response.status, + }) } return memoryEntriesResponseSchema.parse(await response.json()) @@ -466,8 +479,7 @@ export class SupermemoryClient { const status = objectProperty(error, "status") if (typeof status === "number") { - const rawMessage = objectProperty(error, "message") - const message = typeof rawMessage === "string" ? rawMessage : undefined + const message = extractApiErrorMessage(objectProperty(error, "message")) switch (status) { case 400: case 422: @@ -479,7 +491,7 @@ export class SupermemoryClient { case 403: throw new Error( message || - "Access forbidden. Your account may be restricted or blocked.", + "Access forbidden. This connection may be read-only or scoped to specific spaces — reconnect with broader access, or check your account status.", ) case 404: throw new Error("Not found.") From e651045ac50470aa10df5cc8ff7ad2a9b72b00cf Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Sat, 15 Aug 2026 18:41:46 +0530 Subject: [PATCH 21/24] Remove paid plugin UI (#1403) --- apps/web/app/auth/connect/page.tsx | 295 +++++++++--------- apps/web/components/integrations-view.tsx | 5 - .../integrations/plugins-detail.tsx | 59 +--- .../onboarding-brain/step-sources.tsx | 6 +- apps/web/components/select-spaces-modal.tsx | 5 - apps/web/components/settings/billing.tsx | 2 +- 6 files changed, 150 insertions(+), 222 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 0f96e52c..febd2760 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -4,11 +4,10 @@ import { useAuth } from "@lib/auth-context" import { useSession } from "@lib/auth" import { cn } from "@lib/utils" import { dmSans125ClassName } from "@/lib/fonts" -import { useCustomer } from "autumn-js/react" -import { ArrowRight, Loader, XCircle } from "lucide-react" +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" @@ -88,7 +87,7 @@ const PLUGIN_INFO: Record = { "Auto-capture of project decisions", "Context-aware suggestions", ], - icon: "/images/plugins/cursor.svg", + icon: "/images/plugins/cursor.png", }, codex: { name: "OpenAI Codex", @@ -103,11 +102,77 @@ const PLUGIN_INFO: Record = { }, } +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" } -type Status = "loading" | "creating" | "success" | "error" | "upgrade" +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 { + return btoa(JSON.stringify(value)) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, "") +} + +function PluginLogoStack({ clients }: { clients: string[] }) { + if (clients.length === 0) { + return ( +
    + +
    + ) + } + + return ( +
    + {clients.map((id, index) => { + const plugin = PLUGIN_INFO[id] + return ( +
    + {plugin ? ( + {plugin.name} + ) : ( + + )} +
    + ) + })} +
    + ) +} + +type Status = "loading" | "creating" | "success" | "error" const pageWrapperClass = "flex items-center justify-center min-h-screen bg-background p-4" @@ -121,16 +186,34 @@ function AuthConnectContent() { const router = useRouter() const { data: session, isPending } = useSession() const { org, organizations, isRestoring } = useAuth() - const autumn = useCustomer() const [status, setStatus] = useState("loading") const [error, setError] = useState(null) - const [isUpgrading, setIsUpgrading] = useState(false) 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. @@ -166,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( @@ -177,17 +270,13 @@ 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", }) if (!res.ok) { - if (res.status === 403) { - setStatus("upgrade") - return - } const errorData = (await res.json().catch(() => ({}))) as { message?: string } @@ -198,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) { @@ -208,23 +311,23 @@ function AuthConnectContent() { } } - async function handleUpgrade() { - try { - setIsUpgrading(true) - const safeSuccessUrl = `${window.location.origin}${window.location.pathname}?callback=${encodeURIComponent(callback ?? "")}&client=${encodeURIComponent(validClient ?? "")}` - await autumn.attach({ - planId: "api_pro", - successUrl: safeSuccessUrl, - }) - } catch (err) { - console.error("Upgrade failed:", err) - setIsUpgrading(false) - } - } - // 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 (
    @@ -238,19 +341,7 @@ function AuthConnectContent() {
    -
    - {pluginInfo ? ( - {pluginInfo.name} - ) : ( - - )} -
    +

    {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}.`)}

    - {pluginInfo && ( -
      - {pluginInfo.features.map((feature) => ( +
        + {(pluginInfo?.features ?? MULTI_PLUGIN_FEATURES).map( + (feature) => (
      • - ))} -
      - )} + ), + )} +
    - - - View all plans - -
    -
    -
    - ) - } - if (status === "error") { return (
    @@ -435,7 +430,7 @@ function AuthConnectContent() {
    - ))} -
    - ) -} - export function PluginsDetail() { const { org } = useAuth() const autumn = useCustomer() const queryClient = useQueryClient() - const [tierFilter, setTierFilter] = useState("all") const [connectingPlugin, setConnectingPlugin] = useState(null) const [finishSetupPluginId, setFinishSetupPluginId] = useState( null, @@ -572,11 +534,6 @@ export function PluginsDetail() { credentials: "include", }) if (!res.ok) { - if (res.status === 403) { - throw new Error( - "Plugin access was denied. Check your plan or try again.", - ) - } const errorData = (await res.json().catch(() => ({}))) as { message?: string } @@ -635,17 +592,12 @@ export function PluginsDetail() { ) 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( + return [...catalogRows].sort( (a, b) => Number(connectedPluginIds.has(b)) - Number(connectedPluginIds.has(a)), ) - }, [catalogRows, tierFilter, connectedPluginIds]) + }, [catalogRows, connectedPluginIds]) const dialogPlugin = newKey.pluginId ? PLUGIN_CATALOG[newKey.pluginId] @@ -684,12 +636,7 @@ export function PluginsDetail() { )} >
    -
    - Plugins - {catalogRows.length > 0 && ( - - )} -
    + Plugins
    {visibleRows.map((pluginId) => { const plugin = PLUGIN_CATALOG[pluginId] diff --git a/apps/web/components/onboarding-brain/step-sources.tsx b/apps/web/components/onboarding-brain/step-sources.tsx index 5535392d..d3733991 100644 --- a/apps/web/components/onboarding-brain/step-sources.tsx +++ b/apps/web/components/onboarding-brain/step-sources.tsx @@ -149,11 +149,7 @@ const PLAN_CARDS: PlanCardDefinition[] = [ credits: "$20", productId: "api_pro", description: "For people building with AI memory", - features: [ - "Auto top-up when balance runs low", - "All plugins (Claude Code, Cursor, Hermes...)", - "Priority support", - ], + features: ["Auto top-up when balance runs low", "Priority support"], }, { id: "max", diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx index c9a06dd0..7b7398c6 100644 --- a/apps/web/components/select-spaces-modal.tsx +++ b/apps/web/components/select-spaces-modal.tsx @@ -394,11 +394,6 @@ export function SelectSpacesModal({ credentials: "include", }) if (!res.ok) { - if (res.status === 403) { - throw new Error( - "Plugin access was denied. Check your plan or try again.", - ) - } const errorData = (await res.json().catch(() => ({}))) as { message?: string } diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index b46da2c2..b8e366a4 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -137,6 +137,7 @@ const PLAN_CARDS: PlanCardDefinition[] = [ features: [ "Pay-as-you-go after $5 runs out", "Full search and memory access", + "All plugins (Claude Code, Cursor, Hermes...)", "Email support", ], }, @@ -151,7 +152,6 @@ const PLAN_CARDS: PlanCardDefinition[] = [ features: [ "Auto top-up when balance runs low", "Google Drive, Notion, OneDrive & Granola connectors", - "All plugins (Claude Code, Cursor, Hermes...)", "Priority support", ], }, From d14b209f7ca1714568b8e81503090eef276134df Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Sun, 16 Aug 2026 13:43:53 -0700 Subject: [PATCH 22/24] feat(web): support discount code checkout (#1523) --- apps/web/app/(app)/layout.tsx | 2 + apps/web/app/layout.tsx | 2 + .../components/add-document/connections.tsx | 4 + apps/web/components/add-document/index.tsx | 6 ++ apps/web/components/integrations-view.tsx | 6 +- .../integrations/plugins-detail.tsx | 4 + .../onboarding-brain/step-sources.tsx | 4 + apps/web/components/settings/billing.tsx | 4 + .../components/settings/connections-mcp.tsx | 4 + apps/web/hooks/use-promo-code.ts | 82 +++++++++++++++++++ 10 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 apps/web/hooks/use-promo-code.ts diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 700e93a0..7d2d4d97 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -3,10 +3,12 @@ import { EnsureWorkspace } from "@/components/ensure-workspace" import { PWAInstallPrompt } from "@/components/pwa-install-prompt" import { SettingsModalProvider } from "@/components/settings/settings-modal" +import { PromoCodeHost } from "@/hooks/use-promo-code" export default function AppLayout({ children }: { children: React.ReactNode }) { return ( + {children} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 5de4eb43..2ba044fd 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -11,6 +11,7 @@ import { Suspense } from "react" import { Toaster } from "@ui/components/sonner" import { NuqsAdapter } from "nuqs/adapters/next/app" import { ThemeProvider } from "@/lib/theme-provider" +import { PromoCodeCapture } from "@/hooks/use-promo-code" const font = Space_Grotesk({ subsets: ["latin"], @@ -95,6 +96,7 @@ export default function RootLayout({ includeCredentials={true} headers={{ "X-App-Source": "nova" }} > + diff --git a/apps/web/components/add-document/connections.tsx b/apps/web/components/add-document/connections.tsx index 56339a08..8b24a6bd 100644 --- a/apps/web/components/add-document/connections.tsx +++ b/apps/web/components/add-document/connections.tsx @@ -42,6 +42,7 @@ import { getConnectionSubtitle, } from "@/components/settings/sync-utils" import type { ImportProvider } from "@/components/settings/sync-utils" +import { usePromoCode } from "@/hooks/use-promo-code" type GDriveSyncScope = "scoped" | "full" @@ -309,6 +310,7 @@ interface ConnectContentProps { export function ConnectContent({ selectedProject }: ConnectContentProps) { const queryClient = useQueryClient() const autumn = useCustomer() + const promoCode = usePromoCode() const { connectorAccess } = useConnectorAccess() const [connectingProvider, setConnectingProvider] = useState(null) @@ -330,8 +332,10 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { try { const result = await autumn.attach({ planId, + discounts: promoCode.getDiscounts(), successUrl: window.location.href, }) + promoCode.clear() if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") return diff --git a/apps/web/components/add-document/index.tsx b/apps/web/components/add-document/index.tsx index 1c231f7a..6ab77d9a 100644 --- a/apps/web/components/add-document/index.tsx +++ b/apps/web/components/add-document/index.tsx @@ -21,6 +21,7 @@ import { formatUsageNumber } from "@/lib/billing-utils" import { SpaceSelector } from "../space-selector" import { useIsMobile } from "@hooks/use-mobile" import { addDocumentParam } from "@/lib/search-params" +import { usePromoCode } from "@/hooks/use-promo-code" type TabType = "note" | "link" | "file" | "connect" @@ -153,6 +154,7 @@ export function AddDocument({ }) const autumn = useCustomer() + const promoCode = usePromoCode() const { tokensUsed, searchesUsed, @@ -342,8 +344,10 @@ export function AddDocument({ try { const result = await autumn.attach({ planId: "api_pro", + discounts: promoCode.getDiscounts(), successUrl: `${window.location.origin}/settings#account`, }) + promoCode.clear() if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") return @@ -442,8 +446,10 @@ export function AddDocument({ try { const result = await autumn.attach({ planId: "api_pro", + discounts: promoCode.getDiscounts(), successUrl: `${window.location.origin}/settings#account`, }) + promoCode.clear() if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") return diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index 12031cd9..900f4ca6 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -80,6 +80,7 @@ import { import { MCPSteps } from "./mcp-modal/mcp-detail-view" import { GranolaConnectModal } from "./granola-connect-modal" import { detectPluginSpace, detectPluginSource } from "@/lib/plugin-space" +import { usePromoCode } from "@/hooks/use-promo-code" type Connection = z.infer @@ -2555,6 +2556,7 @@ export function IntegrationsView({ const { allProjects } = useContainerTags() const shortcutsConnect = useShortcutsConnect() const autumn = useCustomer({ queryOptions: { enabled: !publicMode } }) + const promoCode = usePromoCode() // connectorAccess covers pro-tier connectors (incl. company_brain orgs); plugins // stay on hasProProduct. See useConnectorAccess. const { hasPro: hasProProduct, connectorAccess } = useConnectorAccess({ @@ -2825,8 +2827,10 @@ export function IntegrationsView({ try { const result = await autumn.attach({ planId: checkoutPlanId, + discounts: promoCode.getDiscounts(), successUrl: `${window.location.origin}/integrations`, }) + promoCode.clear() if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") return @@ -2837,7 +2841,7 @@ export function IntegrationsView({ toast.error("Failed to start checkout. Please try again.") } }, - [autumn], + [autumn, promoCode], ) const redirectToLogin = useCallback(() => { diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx index cb50ecaf..cfcba481 100644 --- a/apps/web/components/integrations/plugins-detail.tsx +++ b/apps/web/components/integrations/plugins-detail.tsx @@ -30,6 +30,7 @@ import { type PluginInfo, } from "@/lib/plugin-catalog" import { INSET, InstallSteps, PillButton } from "./install-steps" +import { usePromoCode } from "@/hooks/use-promo-code" interface ConnectedPlugin { id: string @@ -418,6 +419,7 @@ function PluginRow({ export function PluginsDetail() { const { org } = useAuth() const autumn = useCustomer() + const promoCode = usePromoCode() const queryClient = useQueryClient() const [connectingPlugin, setConnectingPlugin] = useState(null) const [finishSetupPluginId, setFinishSetupPluginId] = useState( @@ -570,8 +572,10 @@ export function PluginsDetail() { try { const result = await autumn.attach({ planId: "api_pro", + discounts: promoCode.getDiscounts(), successUrl: `${window.location.origin}/integrations`, }) + promoCode.clear() if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") return diff --git a/apps/web/components/onboarding-brain/step-sources.tsx b/apps/web/components/onboarding-brain/step-sources.tsx index d3733991..861235e2 100644 --- a/apps/web/components/onboarding-brain/step-sources.tsx +++ b/apps/web/components/onboarding-brain/step-sources.tsx @@ -82,6 +82,7 @@ import { useCustomer } from "autumn-js/react" import { toast } from "sonner" import { analytics } from "@/lib/analytics" import type { BrainMode } from "./types" +import { usePromoCode } from "@/hooks/use-promo-code" type SourceId = | "drive" @@ -614,6 +615,7 @@ function OnboardingPlansModal({ requestedPlan: RequiredPlan }) { const autumn = useCustomer() + const promoCode = usePromoCode() const { currentPlan, isLoading } = useTokenUsage(autumn) const [upgradingPlan, setUpgradingPlan] = useState( null, @@ -632,8 +634,10 @@ function OnboardingPlansModal({ try { const result = await autumn.attach({ planId, + discounts: promoCode.getDiscounts(), successUrl: window.location.href, }) + promoCode.clear() if ((result as { paymentUrl?: string })?.paymentUrl) { window.location.href = (result as { paymentUrl: string }).paymentUrl return diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx index b8e366a4..d69474b9 100644 --- a/apps/web/components/settings/billing.tsx +++ b/apps/web/components/settings/billing.tsx @@ -36,6 +36,7 @@ import { } from "lucide-react" import { useEffect, useMemo, useRef, useState } from "react" import { toast } from "sonner" +import { usePromoCode } from "@/hooks/use-promo-code" const API_BASE = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" @@ -531,6 +532,7 @@ export default function Billing() { const queryClient = useQueryClient() const { user, org } = useAuth() const autumn = useCustomer() + const promoCode = usePromoCode() const posthog = usePostHog() const isCompanyBrain = useHasCompanyBrain() const brainTrial = useMemo( @@ -698,8 +700,10 @@ export default function Billing() { try { const result = await autumn.attach({ planId, + discounts: promoCode.getDiscounts(), successUrl: `${window.location.origin}/settings#billing`, }) + promoCode.clear() if ((result as { paymentUrl?: string })?.paymentUrl) { window.location.href = (result as { paymentUrl: string }).paymentUrl return diff --git a/apps/web/components/settings/connections-mcp.tsx b/apps/web/components/settings/connections-mcp.tsx index ccd098d6..96cd382d 100644 --- a/apps/web/components/settings/connections-mcp.tsx +++ b/apps/web/components/settings/connections-mcp.tsx @@ -38,6 +38,7 @@ import { getConnectionSubtitle, } from "@/components/settings/sync-utils" import type { ImportProvider } from "@/components/settings/sync-utils" +import { usePromoCode } from "@/hooks/use-promo-code" type Connection = z.infer @@ -420,6 +421,7 @@ function FeatureItem({ text }: { text: string }) { export default function ConnectionsMCP() { const queryClient = useQueryClient() const autumn = useCustomer() + const promoCode = usePromoCode() const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam) const router = useRouter() const [removeDialog, setRemoveDialog] = useState<{ @@ -552,8 +554,10 @@ export default function ConnectionsMCP() { try { const result = await autumn.attach({ planId: "api_pro", + discounts: promoCode.getDiscounts(), successUrl: `${window.location.origin}/settings#connections`, }) + promoCode.clear() if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") return diff --git a/apps/web/hooks/use-promo-code.ts b/apps/web/hooks/use-promo-code.ts new file mode 100644 index 00000000..1bc8ca1c --- /dev/null +++ b/apps/web/hooks/use-promo-code.ts @@ -0,0 +1,82 @@ +"use client" + +import { useAuth } from "@lib/auth-context" +import { useRouter } from "next/navigation" +import { useCallback, useEffect, useMemo } from "react" +import { toast } from "sonner" + +const PENDING_PROMO_CODE_KEY = "sm.promoCode.pending" +const PROMO_TOAST_ID = "promo-code" + +function promoCodeKey(orgId: string): string { + return `sm.promoCode.org_${orgId}` +} + +function readOrgPromoCode(orgId?: string): string | null { + if (!orgId || typeof window === "undefined") return null + return window.localStorage.getItem(promoCodeKey(orgId)) +} + +export function usePromoCode() { + const { org } = useAuth() + const orgId = org?.id + + const getDiscounts = useCallback(() => { + const promotionCode = readOrgPromoCode(orgId) + return promotionCode ? [{ promotionCode }] : undefined + }, [orgId]) + + const clear = useCallback(() => { + if (!orgId) return + window.localStorage.removeItem(promoCodeKey(orgId)) + toast.dismiss(PROMO_TOAST_ID) + }, [orgId]) + + return useMemo(() => ({ getDiscounts, clear }), [getDiscounts, clear]) +} + +export function PromoCodeCapture() { + useEffect(() => { + const url = new URL(window.location.href) + const code = url.searchParams.get("discountCode") + if (!code) return + + window.localStorage.setItem(PENDING_PROMO_CODE_KEY, code) + url.searchParams.delete("discountCode") + window.history.replaceState({}, "", url.toString()) + }, []) + + return null +} + +export function PromoCodeHost() { + const { org } = useAuth() + const router = useRouter() + + useEffect(() => { + if (!org?.id) return + + const pending = window.localStorage.getItem(PENDING_PROMO_CODE_KEY) + if (pending) { + window.localStorage.setItem(promoCodeKey(org.id), pending) + window.localStorage.removeItem(PENDING_PROMO_CODE_KEY) + } + + const code = readOrgPromoCode(org.id) + if (!code) { + toast.dismiss(PROMO_TOAST_ID) + return + } + toast.success("Discount code active", { + id: PROMO_TOAST_ID, + description: `Code ${code} will apply at checkout.`, + duration: Number.POSITIVE_INFINITY, + action: { + label: "Upgrade", + onClick: () => router.push("/settings#billing"), + }, + }) + }, [org?.id, router]) + + return null +} From 5d2b5855fe492a3682a1cde4a255e2db0c4db595 Mon Sep 17 00:00:00 2001 From: Dhravya Date: Sun, 16 Aug 2026 23:20:38 +0000 Subject: [PATCH 23/24] feat(auth): AgentID sign-in button on the web login page (#1467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What? Adds a "Continue with AgentID" button to the web app's login page, matching the existing Google/GitHub buttons (same `ExternalAuthButton` pattern, PostHog `login_attempt` capture, last-used badge). - `packages/lib/auth.ts`: adds the `genericOAuthClient` plugin — generic OAuth providers sign in via `signIn.oauth2({ providerId })`, not `signIn.social`. - `apps/web/app/(auth)/login/page.tsx`: the button, gated the same way as the other social buttons — always shown on cloud (`NEXT_PUBLIC_HOST_ID === "supermemory"`), opt-in elsewhere via `NEXT_PUBLIC_AGENTID_AUTH_ENABLED` (added to `.env.example`). ## Why? Companion to supermemoryai/mono#2908, which registers an `agentid` generic OAuth provider (OIDC against auth.agentid.com) on the API so agents can authenticate with their AgentID identity. The consumer app talks to the same better-auth server, so it gets the same sign-in option. mono#2916 additionally auto-invites the agent's verified human owner to the agent's workspace. Requires mono#2908 to be deployed for the button to work; until then the API rejects the unknown provider and the page shows its normal error state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **Medium Risk** > Touches authentication entry points and OAuth client configuration; risk is moderate because it extends login surface area but follows existing social sign-in patterns and is feature-flagged. > > **Overview** > Adds **Continue with AgentID** on the web login page, using the same `ExternalAuthButton` flow as Google/GitHub (PostHog `login_attempt`, last-used badge, loading/error handling). > > The button calls **`signIn.oauth2({ providerId: "agentid" })`** instead of `signIn.social`, enabled by registering **`genericOAuthClient`** on the shared better-auth client in `packages/lib/auth.ts`. > > Visibility matches other social providers: shown on cloud when `NEXT_PUBLIC_HOST_ID === "supermemory"`, or elsewhere when **`NEXT_PUBLIC_AGENTID_AUTH_ENABLED`** is set (documented in `.env.example`). Depends on the API registering the `agentid` generic OAuth provider. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 90a32786a380d804b4833a0dbefd620072952b27. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- apps/web/.env.example | 3 +- apps/web/app/(auth)/login/page.tsx | 73 ++++++++++++++++++++++++++++++ packages/lib/auth.ts | 2 + 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/apps/web/.env.example b/apps/web/.env.example index aaf5fab4..abd39cef 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,4 +1,5 @@ NEXT_PUBLIC_BACKEND_URL=https://api.supermemory.ai NEXT_PUBLIC_POSTHOG_KEY= EXA_API_KEY= -XAI_API_KEY= \ No newline at end of file +XAI_API_KEY= +NEXT_PUBLIC_AGENTID_AUTH_ENABLED= diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index 8500a236..442ded03 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -591,6 +591,79 @@ export default function LoginPage() { />
    ) : null} + {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || + process.env.NEXT_PUBLIC_AGENTID_AUTH_ENABLED ? ( +
    + + + AgentID + + + + + + + } + authProvider="AgentID" + className="w-full" + disabled={Boolean(loadingMessage)} + onClick={() => { + if (loadingMessage) return + setIsLoading(true) + posthog.capture("login_attempt", { + method: "social", + provider: "agentid", + }) + setPendingLoginMethod("agentid") + signIn + .oauth2({ + callbackURL: getCallbackURL(), + providerId: "agentid", + }) + .catch((err: unknown) => { + setError(getErrorMessage(err)) + setIsLoading(false) + }) + }} + /> +
    + ) : null}
    Date: Mon, 17 Aug 2026 18:47:36 +0000 Subject: [PATCH 24/24] Fix integrations layout and mobile promo responsiveness (#1481) ## Summary - Reorder Apps & extensions so Import X bookmarks appears in the top row and Apple Shortcuts uses the open space below. - Keep both Apple Shortcut actions inline on larger screens while allowing the card to grow only as much as needed. - Rework the Company Brain promo on phones so its logo, copy, close control, and CTA remain readable and aligned. --- apps/web/components/company-brain-promo.tsx | 52 ++++++++------- apps/web/components/integrations-view.tsx | 66 +++++++++++++------ .../integrations/shortcuts-detail.tsx | 2 +- 3 files changed, 75 insertions(+), 45 deletions(-) diff --git a/apps/web/components/company-brain-promo.tsx b/apps/web/components/company-brain-promo.tsx index 730eeb08..3bda29a8 100644 --- a/apps/web/components/company-brain-promo.tsx +++ b/apps/web/components/company-brain-promo.tsx @@ -45,42 +45,44 @@ export function CompanyBrainPromo() { return (
    -
    +
    -
    -

    - Give your team a Company Brain -

    -

    - Lives in your Slack. Answers from your team's tools, and brings things - up before you ask. -

    +
    +
    +

    + Give your team a Company Brain +

    +

    + Lives in your Slack. Answers from your team's tools, and brings + things up before you ask. +

    +
    +
    - diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index 900f4ca6..19a3b9b0 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -537,13 +537,13 @@ const SECTIONS: Array<{ action: { type: "external", href: POKE_RECIPE_URL }, }, { - kind: "client", - id: "shortcuts", - name: "Apple Shortcuts", - tagline: "Add memories from iPhone, iPad or Mac", - simpleTitle: "Save anything from your phone or Mac", - icon: , - action: { type: "view", viewMode: "shortcuts" as ViewParamValue }, + kind: "import", + id: "x-bookmarks", + name: "Import X bookmarks", + tagline: "Turn your X/Twitter bookmarks into memories", + simpleTitle: "Turn your X bookmarks into memory", + icon: X, + viewMode: "import" as ViewParamValue, }, { kind: "client", @@ -556,13 +556,13 @@ const SECTIONS: Array<{ dev: true, }, { - kind: "import", - id: "x-bookmarks", - name: "Import X bookmarks", - tagline: "Turn your X/Twitter bookmarks into memories", - simpleTitle: "Turn your X bookmarks into memory", - icon: X, - viewMode: "import" as ViewParamValue, + kind: "client", + id: "shortcuts", + name: "Apple Shortcuts", + tagline: "Add memories from iPhone, iPad or Mac", + simpleTitle: "Save anything from your phone or Mac", + icon: , + action: { type: "view", viewMode: "shortcuts" as ViewParamValue }, }, ], }, @@ -2068,6 +2068,7 @@ function ItemCard({ docsUrl, leftIndicator, statusSlot, + layoutClassName, }: { actionSlot: ReactNode infoActionSlot?: ReactNode @@ -2082,6 +2083,7 @@ function ItemCard({ docsUrl?: string leftIndicator?: ReactNode statusSlot?: ReactNode + layoutClassName?: string }) { const [infoOpen, setInfoOpen] = useState(false) return ( @@ -2099,6 +2101,9 @@ function ItemCard({ className={cn( "group relative flex h-full cursor-pointer flex-row items-center gap-2.5 rounded-[10px] bg-[#14161A] px-2.5 py-2 transition-colors hover:bg-[#16181D] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#4BA0FA]/45 sm:flex-col sm:items-stretch sm:gap-4 sm:rounded-[12px] sm:p-4", "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]", + id === "shortcuts" && + "max-sm:grid max-sm:grid-cols-[auto_minmax(0,1fr)] max-sm:items-center", + layoutClassName, )} > setInfoOpen(true)} /> @@ -2116,7 +2121,12 @@ function ItemCard({
    {icon}
    -
    +
    {leftIndicator} @@ -2140,7 +2150,13 @@ function ItemCard({ {tagline}

    -
    +
    {/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the status action. */}
    {/* biome-ignore lint/a11y/noStaticElementInteractions: stop card click from swallowing the primary action. */}
    button]:!h-7 [&>button]:!min-w-[82px] [&>button]:!px-3 [&>button]:!text-[11px] sm:[&>button]:!h-9 sm:[&>button]:!min-w-[116px] sm:[&>button]:!px-5 sm:[&>button]:!text-[14px]", + id === "shortcuts" && + "max-sm:w-full max-sm:shrink max-sm:[&>div]:w-full", + )} onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} > @@ -3628,7 +3648,7 @@ export function IntegrationsView({ } } - const renderItemCard = (item: Item) => ( + const renderItemCard = (item: Item, layoutClassName?: string) => ( ) @@ -3753,7 +3774,14 @@ export function IntegrationsView({

    ) : q || category !== "all" ? (
    - {visibleItems.map((item) => renderItemCard(item))} + {visibleItems.map((item) => + renderItemCard( + item, + item.id === "shortcuts" + ? "sm:w-max sm:min-w-full" + : undefined, + ), + )}
    ) : (
    diff --git a/apps/web/components/integrations/shortcuts-detail.tsx b/apps/web/components/integrations/shortcuts-detail.tsx index 833488eb..3737fd36 100644 --- a/apps/web/components/integrations/shortcuts-detail.tsx +++ b/apps/web/components/integrations/shortcuts-detail.tsx @@ -151,7 +151,7 @@ export function ShortcutsConnectButtons({ }) { const { connect, isPending, pendingType } = controller return ( -
    +
    {