diff --git a/apps/web/components/chat/message/agent-message.tsx b/apps/web/components/chat/message/agent-message.tsx index c7d76485..96696785 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;