From 70bb21c6e512afe3cbca44eab6a2a296a662534e Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Tue, 9 Jun 2026 14:56:23 +0530 Subject: [PATCH 1/6] Render Nova connector setup cards --- apps/web/components/chat/chat-empty-state.tsx | 4 +- .../components/chat/message/agent-message.tsx | 388 ++++++++++++++++++ 2 files changed, 390 insertions(+), 2 deletions(-) diff --git a/apps/web/components/chat/chat-empty-state.tsx b/apps/web/components/chat/chat-empty-state.tsx index 1fe230bf3..bef9894a4 100644 --- a/apps/web/components/chat/chat-empty-state.tsx +++ b/apps/web/components/chat/chat-empty-state.tsx @@ -7,8 +7,8 @@ import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" export const DEFAULT_CHAT_PROMPTS = [ "What do you know about me?", - "What have I been working on lately?", - "What themes keep showing up in my memories?", + "Set up Cursor", + "Show my active plugins", ] as const const SUGGESTION_PILL_CLASS = cn( diff --git a/apps/web/components/chat/message/agent-message.tsx b/apps/web/components/chat/message/agent-message.tsx index 1b7f69a82..cf047c68e 100644 --- a/apps/web/components/chat/message/agent-message.tsx +++ b/apps/web/components/chat/message/agent-message.tsx @@ -5,10 +5,14 @@ import type { UIMessage } from "@ai-sdk/react" import { Streamdown } from "streamdown" import { BookOpenIcon, + CheckIcon, ChevronDownIcon, ChevronRightIcon, ClockIcon, + CopyIcon, + ExternalLinkIcon, GlobeIcon, + KeyRoundIcon, ListIcon, Loader2, PlusIcon, @@ -16,6 +20,7 @@ import { TerminalIcon, WrenchIcon, XCircleIcon, + ZapIcon, } from "lucide-react" import { cn } from "@lib/utils" import { isWebSearchToolName } from "@/lib/chat-web-search-tools" @@ -64,6 +69,386 @@ function faviconUrl(host: string): string { return `https://www.google.com/s2/favicons?sz=64&domain=${host}` } +type NovaConnectorStatus = + | "active" + | "setup_pending" + | "not_connected" + | "upgrade_required" + | "setup_available" + +type NovaConnectorStep = { + title?: string + description?: string + code?: string + link?: { url: string; label: string } + createPluginKey?: boolean +} + +type NovaConnectorCardData = { + kind?: "plugin" | "mcp" + id?: string + name?: string + icon?: string + description?: string + features?: string[] + docsUrl?: string + repoUrl?: string + installSteps?: NovaConnectorStep[] + status?: NovaConnectorStatus + requiresPro?: boolean + canGenerateKey?: boolean + keyPluginId?: string +} + +type NovaConnectorToolOutput = { + success?: boolean + error?: string + kind?: string + connectors?: NovaConnectorCardData[] + connector?: NovaConnectorCardData + keyReveal?: { pluginId: string; label?: string } | null + available?: Array<{ kind: "plugin" | "mcp"; id: string; name: string }> +} + +const NOVA_CONNECTOR_TOOLS = new Set([ + "listNovaConnectors", + "getNovaConnectorSetup", + "prepareNovaPluginSetup", +]) + +const STATUS_COPY: Record< + NovaConnectorStatus, + { label: string; className: string } +> = { + active: { + label: "Active", + className: "border-emerald-400/20 bg-emerald-400/10 text-emerald-300", + }, + setup_pending: { + label: "Finish setup", + className: "border-amber-400/20 bg-amber-400/10 text-amber-300", + }, + not_connected: { + label: "Not connected", + className: "border-white/10 bg-white/[0.05] text-white/55", + }, + upgrade_required: { + label: "Pro required", + className: "border-[#4BA0FA]/25 bg-[#4BA0FA]/10 text-[#4BA0FA]", + }, + setup_available: { + label: "Setup available", + className: "border-white/10 bg-white/[0.05] text-white/65", + }, +} + +function unwrapToolOutput(output: unknown): NovaConnectorToolOutput | null { + if (typeof output === "string") { + try { + return JSON.parse(output) as NovaConnectorToolOutput + } catch { + return null + } + } + if (!output || typeof output !== "object") return null + const record = output as Record + if ( + record.type === "json" && + record.value && + typeof record.value === "object" + ) { + return record.value as NovaConnectorToolOutput + } + if (record.type === "text" && typeof record.value === "string") { + try { + return JSON.parse(record.value) as NovaConnectorToolOutput + } catch { + return null + } + } + return record as NovaConnectorToolOutput +} + +function StatusPill({ status }: { status?: NovaConnectorStatus }) { + const copy = STATUS_COPY[status ?? "not_connected"] + return ( + + {copy.label} + + ) +} + +function MiniCopyButton({ text, label }: { text: string; label?: string }) { + const [copied, setCopied] = useState(false) + return ( + + ) +} + +function ConnectorCodeBlock({ + code, + apiKey, +}: { + code: string + apiKey?: string +}) { + const rendered = apiKey ? code.replaceAll("sm_...", apiKey) : code + return ( +
+
+				{rendered}
+			
+ +
+ ) +} + +function RevealPluginKeyButton({ + pluginId, + onReveal, +}: { + pluginId: string + onReveal: (key: string) => void +}) { + const [state, setState] = useState<"idle" | "loading" | "copied" | "error">( + "idle", + ) + return ( + + ) +} + +function NovaConnectorCard({ + connector, +}: { + connector: NovaConnectorCardData +}) { + const [revealedKey, setRevealedKey] = useState() + const needsKey = Boolean(connector.canGenerateKey && connector.keyPluginId) + const isUpgrade = connector.status === "upgrade_required" + return ( +
+
+
+ {connector.icon ? ( + + ) : ( + + )} +
+
+
+

+ {connector.name ?? connector.id ?? "Connector"} +

+ +
+ {connector.description ? ( +

+ {connector.description} +

+ ) : null} +
+
+ + {connector.installSteps?.length ? ( +
    + {connector.installSteps.map((step, index) => ( +
  1. + + {index + 1} + +
    +

    + {step.title} +

    + {step.description ? ( +

    + {step.description} +

    + ) : null} + {step.code ? ( + + ) : null} + {step.link ? ( + + {step.link.label} + + + ) : null} +
    +
  2. + ))} +
+ ) : null} + +
+ {needsKey && connector.keyPluginId && !isUpgrade ? ( + + ) : null} + {isUpgrade ? ( + + + Upgrade to connect + + ) : null} + {connector.docsUrl ? ( + + + Docs + + ) : null} +
+
+ ) +} + +function NovaConnectorToolDisplay({ part }: { part: ToolCallDisplayPart }) { + const toolName = part.type.replace("tool-", "") + const output = unwrapToolOutput(part.output) + const isLoading = + part.state === "input-streaming" || part.state === "input-available" + const isError = part.state === "error" || part.state === "output-error" + if (isLoading) { + return ( +
+ + Checking Supermemory setup… +
+ ) + } + if (isError || !output) { + return ( +
+ Couldn't load connector setup. +
+ ) + } + if (output.success === false) { + return ( +
+

+ {output.error ?? "Connector not found"} +

+ {output.available?.length ? ( +

+ Try one of: {output.available.map((item) => item.name).join(", ")} +

+ ) : null} +
+ ) + } + const connectors = output.connector + ? [output.connector] + : (output.connectors ?? []) + return ( +
+ {toolName === "listNovaConnectors" && connectors.length > 1 ? ( +

+ Supermemory setup options +

+ ) : null} + {connectors.map((connector) => ( + + ))} +
+ ) +} + function isWebSearchPart(part: { type: string; toolName?: string }): boolean { if (part.type === "dynamic-tool") { return isWebSearchToolName(part.toolName ?? "") @@ -364,6 +749,9 @@ function BashToolDisplay({ part }: { part: ToolCallDisplayPart }) { function ToolCallDisplay({ part }: { part: ToolCallDisplayPart }) { const [expanded, setExpanded] = useState(false) const toolName = part.type.replace("tool-", "") + if (NOVA_CONNECTOR_TOOLS.has(toolName)) { + return + } if (toolName === "bash") { return } From e9165a323681d42fcb55b69529688c95c578bc72 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Tue, 9 Jun 2026 18:07:27 +0530 Subject: [PATCH 2/6] Harden Nova connector tool rendering --- .../components/chat/message/agent-message.tsx | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/apps/web/components/chat/message/agent-message.tsx b/apps/web/components/chat/message/agent-message.tsx index cf047c68e..c7d76485f 100644 --- a/apps/web/components/chat/message/agent-message.tsx +++ b/apps/web/components/chat/message/agent-message.tsx @@ -142,16 +142,33 @@ const STATUS_COPY: Record< }, } +function connectorToolName(part: ToolCallDisplayPart): string { + return part.type.startsWith("tool-") + ? part.type.slice("tool-".length) + : part.type +} + +function parseConnectorOutput(value: string): NovaConnectorToolOutput | null { + try { + return JSON.parse(value) as NovaConnectorToolOutput + } catch { + return null + } +} + function unwrapToolOutput(output: unknown): NovaConnectorToolOutput | null { if (typeof output === "string") { - try { - return JSON.parse(output) as NovaConnectorToolOutput - } catch { - return null - } + return parseConnectorOutput(output) } if (!output || typeof output !== "object") return null const record = output as Record + for (const key of ["value", "result", "data", "output"]) { + const nested = record[key] + if (nested && nested !== output) { + const parsed = unwrapToolOutput(nested) + if (parsed) return parsed + } + } if ( record.type === "json" && record.value && @@ -160,17 +177,17 @@ function unwrapToolOutput(output: unknown): NovaConnectorToolOutput | null { return record.value as NovaConnectorToolOutput } if (record.type === "text" && typeof record.value === "string") { - try { - return JSON.parse(record.value) as NovaConnectorToolOutput - } catch { - return null - } + return parseConnectorOutput(record.value) + } + if (typeof record.text === "string") { + return parseConnectorOutput(record.text) } return record as NovaConnectorToolOutput } function StatusPill({ status }: { status?: NovaConnectorStatus }) { - const copy = STATUS_COPY[status ?? "not_connected"] + const copy = + STATUS_COPY[status ?? "not_connected"] ?? STATUS_COPY.not_connected return ( ) } - if (isError || !output) { + if (isError) { return (
Couldn't load connector setup.
) } + if (!output) return null if (output.success === false) { return (
@@ -748,7 +766,7 @@ function BashToolDisplay({ part }: { part: ToolCallDisplayPart }) { function ToolCallDisplay({ part }: { part: ToolCallDisplayPart }) { const [expanded, setExpanded] = useState(false) - const toolName = part.type.replace("tool-", "") + const toolName = connectorToolName(part) if (NOVA_CONNECTOR_TOOLS.has(toolName)) { return } From 261dea948c5b95622b825db8f4d53cf453774608 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Tue, 9 Jun 2026 18:57:06 +0530 Subject: [PATCH 3/6] Polish Nova connector chat cards --- .../components/chat/message/agent-message.tsx | 185 ++++++++++++++++-- apps/web/globals.css | 30 +++ 2 files changed, 201 insertions(+), 14 deletions(-) diff --git a/apps/web/components/chat/message/agent-message.tsx b/apps/web/components/chat/message/agent-message.tsx index c7d76485f..96696785d 100644 --- a/apps/web/components/chat/message/agent-message.tsx +++ b/apps/web/components/chat/message/agent-message.tsx @@ -12,7 +12,6 @@ import { CopyIcon, ExternalLinkIcon, GlobeIcon, - KeyRoundIcon, ListIcon, Loader2, PlusIcon, @@ -116,6 +115,12 @@ const NOVA_CONNECTOR_TOOLS = new Set([ "prepareNovaPluginSetup", ]) +const CONNECTOR_ICON_FALLBACKS: Record = { + codex: "/images/plugins/codex.png", + cursor: "/images/plugins/cursor.png", + mcp_cursor: "/mcp-supported-tools/cursor.png", +} + const STATUS_COPY: Record< NovaConnectorStatus, { label: string; className: string } @@ -148,6 +153,14 @@ function connectorToolName(part: ToolCallDisplayPart): string { : part.type } +function connectorToolNameFromPart(part: unknown): string | null { + if (!part || typeof part !== "object") return null + const record = part as { type?: string; toolName?: string } + if (record.type === "dynamic-tool") return record.toolName ?? null + if (record.type?.startsWith("tool-")) return record.type.slice("tool-".length) + return null +} + function parseConnectorOutput(value: string): NovaConnectorToolOutput | null { try { return JSON.parse(value) as NovaConnectorToolOutput @@ -185,6 +198,70 @@ function unwrapToolOutput(output: unknown): NovaConnectorToolOutput | null { return record as NovaConnectorToolOutput } +function connectorIconSrc( + connector: NovaConnectorCardData, +): string | undefined { + if (connector.id && CONNECTOR_ICON_FALLBACKS[connector.id]) { + return CONNECTOR_ICON_FALLBACKS[connector.id] + } + if (connector.icon?.endsWith("/codex.svg")) + return CONNECTOR_ICON_FALLBACKS.codex + if (connector.icon?.endsWith("/cursor.svg")) + return CONNECTOR_ICON_FALLBACKS.cursor + return connector.icon +} + +function connectorIdentity( + output: NovaConnectorToolOutput | null, +): string | null { + if (!output) return null + if (output.connectors && output.connectors.length !== 1) return null + const connector = output.connector ?? output.connectors?.[0] + if (!connector) return null + return `${connector.kind ?? "connector"}:${connector.id ?? connector.name ?? ""}` +} + +function connectorOutputFromPart( + part: unknown, +): NovaConnectorToolOutput | null { + if (!part || typeof part !== "object") return null + const record = part as { + type?: string + toolName?: string + output?: unknown + } + const toolName = connectorToolNameFromPart(record) + if (!toolName || !NOVA_CONNECTOR_TOOLS.has(toolName)) return null + return unwrapToolOutput(record.output) +} + +function connectorToolPriority(toolName: string | null): number { + if (toolName === "prepareNovaPluginSetup") return 2 + if (toolName === "getNovaConnectorSetup") return 1 + return 0 +} + +function shouldSkipNovaConnectorPart(parts: unknown[], index: number): boolean { + const part = parts[index] + const toolName = connectorToolNameFromPart(part) + if (!toolName || !NOVA_CONNECTOR_TOOLS.has(toolName)) return false + const identity = connectorIdentity(connectorOutputFromPart(part)) + if (!identity) return false + const priority = connectorToolPriority(toolName) + + for (let i = 0; i < parts.length; i++) { + if (i === index) continue + const otherTool = connectorToolNameFromPart(parts[i]) + if (!otherTool || !NOVA_CONNECTOR_TOOLS.has(otherTool)) continue + const otherIdentity = connectorIdentity(connectorOutputFromPart(parts[i])) + if (otherIdentity !== identity) continue + const otherPriority = connectorToolPriority(otherTool) + if (i < index && otherPriority >= priority) return true + if (i > index && otherPriority > priority) return true + } + return false +} + function StatusPill({ status }: { status?: NovaConnectorStatus }) { const copy = STATUS_COPY[status ?? "not_connected"] ?? STATUS_COPY.not_connected @@ -297,14 +374,14 @@ function RevealPluginKeyButton({ ) : state === "error" ? ( - ) : ( - - )} + ) : null} {state === "copied" ? "Key copied" : state === "error" ? "Try again" - : "Generate / Reveal key"} + : state === "loading" + ? "Generating" + : "Generate key"} ) } @@ -317,15 +394,28 @@ function NovaConnectorCard({ const [revealedKey, setRevealedKey] = useState() const needsKey = Boolean(connector.canGenerateKey && connector.keyPluginId) const isUpgrade = connector.status === "upgrade_required" + const iconSrc = connectorIconSrc(connector) return (
- {connector.icon ? ( + {iconSrc ? ( { + const img = event.currentTarget + const fallback = connector.id + ? CONNECTOR_ICON_FALLBACKS[connector.id] + : undefined + if (fallback && img.dataset.fallbackApplied !== "true") { + img.dataset.fallbackApplied = "true" + img.src = fallback + } else { + img.style.display = "none" + } + }} /> ) : ( @@ -411,6 +501,51 @@ function NovaConnectorCard({ ) } +function NovaConnectorCompactCard({ + connector, +}: { + connector: NovaConnectorCardData +}) { + const iconSrc = connectorIconSrc(connector) + return ( +
+ +
+
+ +
+
+
+ ) +} + function NovaConnectorToolDisplay({ part }: { part: ToolCallDisplayPart }) { const toolName = connectorToolName(part) const output = unwrapToolOutput(part.output) @@ -450,19 +585,35 @@ function NovaConnectorToolDisplay({ part }: { part: ToolCallDisplayPart }) { const connectors = output.connector ? [output.connector] : (output.connectors ?? []) + const isConnectorList = + toolName === "listNovaConnectors" && connectors.length > 1 return (
- {toolName === "listNovaConnectors" && connectors.length > 1 ? ( + {isConnectorList ? (

Supermemory setup options

) : null} - {connectors.map((connector) => ( - - ))} +
+ {connectors.map((connector) => + isConnectorList ? ( + + ) : ( + + ), + )} +
) } @@ -1015,6 +1166,9 @@ export function AgentMessage({ ) } if (part.type === "dynamic-tool") { + if (shouldSkipNovaConnectorPart(message.parts, partIndex)) { + return null + } const dt = part as { type: "dynamic-tool" toolName: string @@ -1042,6 +1196,9 @@ export function AgentMessage({ ) } if (part.type.startsWith("tool-")) { + if (shouldSkipNovaConnectorPart(message.parts, partIndex)) { + return null + } return ( code { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 0.375rem; + background: rgba(255, 255, 255, 0.06); + padding: 0.125rem 0.3125rem; + color: #fafafa; + font-size: 0.875em; +} + /* Model spams `---` between every section; headings already separate them */ .chat-markdown-content hr { display: none; From e9bff4abad7af23677e4540100588797e69cfd32 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Tue, 9 Jun 2026 19:05:27 +0530 Subject: [PATCH 4/6] Contain Nova connector hover cards --- apps/web/components/chat/message/agent-message.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/web/components/chat/message/agent-message.tsx b/apps/web/components/chat/message/agent-message.tsx index 96696785d..09add29bb 100644 --- a/apps/web/components/chat/message/agent-message.tsx +++ b/apps/web/components/chat/message/agent-message.tsx @@ -508,7 +508,7 @@ function NovaConnectorCompactCard({ }) { const iconSrc = connectorIconSrc(connector) return ( -
+
-
-
- -
+
+
) From c160453be8a7d63fbb22ccb6aea19294c6fe5937 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Tue, 9 Jun 2026 19:09:13 +0530 Subject: [PATCH 5/6] Improve chat code block contrast --- apps/web/globals.css | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/web/globals.css b/apps/web/globals.css index b02fc2141..ff8d4e904 100644 --- a/apps/web/globals.css +++ b/apps/web/globals.css @@ -202,20 +202,31 @@ border-radius: 0.75rem; background: #080b10 !important; padding: 0.875rem 1rem; - color: #e4e4e7 !important; + color: #f4f4f5 !important; } .chat-markdown-content pre code { display: block; background: transparent !important; padding: 0; - color: #e4e4e7 !important; - font-size: 0.8125rem; + color: #f4f4f5 !important; + font-size: 0.875rem; line-height: 1.65; text-shadow: none; white-space: pre; } +.chat-markdown-content pre code *, +.chat-markdown-content pre [style] { + background: transparent !important; + color: #f4f4f5 !important; + text-shadow: none !important; +} + +.chat-markdown-content pre code .line { + min-height: 1.45em; +} + .chat-markdown-content :not(pre) > code { border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 0.375rem; From fd98c5e11ad750388b1b11df4d9ab05839fc3d0c Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Tue, 9 Jun 2026 19:17:08 +0530 Subject: [PATCH 6/6] Make Nova connector list click expandable --- .../components/chat/message/agent-message.tsx | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/apps/web/components/chat/message/agent-message.tsx b/apps/web/components/chat/message/agent-message.tsx index 09add29bb..b30b92f79 100644 --- a/apps/web/components/chat/message/agent-message.tsx +++ b/apps/web/components/chat/message/agent-message.tsx @@ -211,6 +211,10 @@ function connectorIconSrc( return connector.icon } +function connectorCardKey(connector: NovaConnectorCardData): string { + return `${connector.kind ?? "connector"}-${connector.id ?? connector.name ?? "unknown"}` +} + function connectorIdentity( output: NovaConnectorToolOutput | null, ): string | null { @@ -503,15 +507,26 @@ function NovaConnectorCard({ function NovaConnectorCompactCard({ connector, + expanded, + onToggle, }: { connector: NovaConnectorCardData + expanded: boolean + onToggle: () => void }) { const iconSrc = connectorIconSrc(connector) return ( -
+
-
- -
) } function NovaConnectorToolDisplay({ part }: { part: ToolCallDisplayPart }) { + const [expandedConnectorKey, setExpandedConnectorKey] = useState< + string | null + >(null) const toolName = connectorToolName(part) const output = unwrapToolOutput(part.output) const isLoading = @@ -585,6 +605,12 @@ function NovaConnectorToolDisplay({ part }: { part: ToolCallDisplayPart }) { : (output.connectors ?? []) const isConnectorList = toolName === "listNovaConnectors" && connectors.length > 1 + const expandedConnector = + isConnectorList && expandedConnectorKey + ? connectors.find( + (connector) => connectorCardKey(connector) === expandedConnectorKey, + ) + : null return (
{isConnectorList ? ( @@ -601,17 +627,29 @@ function NovaConnectorToolDisplay({ part }: { part: ToolCallDisplayPart }) { {connectors.map((connector) => isConnectorList ? ( { + const nextKey = connectorCardKey(connector) + setExpandedConnectorKey((current) => + current === nextKey ? null : nextKey, + ) + }} /> ) : ( ), )}
+ {expandedConnector ? ( +
+ +
+ ) : null}
) }