From fd7f767545ea39f4b35cfb95cffd482bfcff9dd2 Mon Sep 17 00:00:00 2001 From: rafavalls Date: Fri, 27 Feb 2026 08:59:02 -0300 Subject: [PATCH] feat(agents): redesign Connect button and share modal - Replace lightning icon with overlapping Cursor/Claude branded icons on Connect button - Redesign client action cards with colored backgrounds and white logos - Replace Windsurf card with generic Copy URL action - Make typegen section collapsible by default - Fix mode selection highlighting with state-driven bg-accent/30 - Reduce radio card padding and gap for tighter layout Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../components/details/virtual-mcp/index.tsx | 26 +- .../virtual-mcp/virtual-mcp-share-modal.tsx | 452 +++++++++--------- 2 files changed, 248 insertions(+), 230 deletions(-) diff --git a/apps/mesh/src/web/components/details/virtual-mcp/index.tsx b/apps/mesh/src/web/components/details/virtual-mcp/index.tsx index d133014945..efdad472e2 100644 --- a/apps/mesh/src/web/components/details/virtual-mcp/index.tsx +++ b/apps/mesh/src/web/components/details/virtual-mcp/index.tsx @@ -41,7 +41,6 @@ import { Loading01, Play, Plus, - ZapCircle, Tool01, Users03, } from "@untitledui/icons"; @@ -316,7 +315,30 @@ function VirtualMcpDetailViewWithData({ dispatch({ type: "SET_SHARE_DIALOG_OPEN", payload: true }) } > - +
+
+ Cursor +
+
+ Claude Code +
+
Connect diff --git a/apps/mesh/src/web/components/details/virtual-mcp/virtual-mcp-share-modal.tsx b/apps/mesh/src/web/components/details/virtual-mcp/virtual-mcp-share-modal.tsx index 3f97293d36..ed5d6dc8c7 100644 --- a/apps/mesh/src/web/components/details/virtual-mcp/virtual-mcp-share-modal.tsx +++ b/apps/mesh/src/web/components/details/virtual-mcp/virtual-mcp-share-modal.tsx @@ -26,12 +26,14 @@ import type { VirtualMCPEntity } from "@decocms/mesh-sdk"; import { ArrowsRight, Check, + ChevronDown, Code01, Copy01, InfoCircle, Key01, Lightbulb02, Loading01, + Terminal, } from "@untitledui/icons"; import { cn } from "@deco/ui/lib/utils.ts"; import { Suspense, useState } from "react"; @@ -49,140 +51,81 @@ function utf8ToBase64(str: string): string { return btoa(binary); } -/** - * Shared button props interfaces - */ -interface ShareButtonProps { - url: string; -} - -interface ShareWithNameProps extends ShareButtonProps { - serverName: string; -} +type AgentMode = "passthrough" | "smart_tool_selection" | "code_execution"; /** - * Copy URL Button Component + * Client card — colored icon bg + white logo + one-click action */ -function CopyUrlButton({ url }: ShareButtonProps) { - const [copied, setCopied] = useState(false); - - const handleCopy = async () => { - await navigator.clipboard.writeText(url); - setCopied(true); - toast.success("Agent URL copied to clipboard"); - setTimeout(() => setCopied(false), 2000); - }; - +function ClientCard({ + logo, + alt, + label, + bgColor, + onClick, + copied, +}: { + logo: string; + alt: string; + label: string; + bgColor: string; + onClick: () => void; + copied?: boolean; +}) { return ( - + ); } /** - * Install on Cursor Button Component + * Copy URL card — uses Copy icon instead of a logo */ -function InstallCursorButton({ url, serverName }: ShareWithNameProps) { - const handleInstall = () => { - const slugifiedServerName = slugify(serverName); - const connectionConfig = { - type: "http", - url: url, - headers: { - "x-mesh-client": "Cursor", - }, - }; - const base64Config = utf8ToBase64( - JSON.stringify(connectionConfig, null, 2), - ); - const deeplink = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(slugifiedServerName)}&config=${encodeURIComponent(base64Config)}`; - - window.open(deeplink, "_blank"); - toast.success("Opening Cursor..."); - }; - - return ( - - ); -} - -/** - * Install on Claude Code Button Component - */ -function InstallClaudeButton({ url, serverName }: ShareWithNameProps) { - const [copied, setCopied] = useState(false); - - const handleInstall = async () => { - const slugifiedServerName = slugify(serverName); - const connectionConfig = { - type: "http", - url: url, - headers: { - "x-mesh-client": "Claude Code", - }, - }; - const configJson = JSON.stringify(connectionConfig, null, 2); - const command = `claude mcp add-json "${slugifiedServerName}" '${configJson.replace(/'/g, "'\\''")}'`; - - await navigator.clipboard.writeText(command); - setCopied(true); - toast.success("Claude Code command copied to clipboard"); - setTimeout(() => setCopied(false), 2000); - }; - +function CopyUrlCard({ + onClick, + copied, +}: { + onClick: () => void; + copied?: boolean; +}) { return ( - + ); } @@ -197,7 +140,8 @@ function TypegenSectionInner({ virtualMcp }: { virtualMcp: VirtualMCPEntity }) { }); const [apiKey, setApiKey] = useState(null); const [generating, setGenerating] = useState(false); - const [copied, setCopied] = useState(false); + const [copiedCmd, setCopiedCmd] = useState(false); + const [copiedEnv, setCopiedEnv] = useState(false); const mcpId = virtualMcp.id; const agentName = virtualMcp.title || `agent-${mcpId.slice(0, 8)}`; @@ -205,6 +149,11 @@ function TypegenSectionInner({ virtualMcp }: { virtualMcp: VirtualMCPEntity }) { ? `bunx @decocms/typegen@latest --mcp ${mcpId} --key ${apiKey} --output client.ts` : `bunx @decocms/typegen@latest --mcp ${mcpId} --key --output client.ts`; + const meshUrl = window.location.origin; + const keyLine = apiKey ? `MESH_API_KEY=${apiKey}` : `MESH_API_KEY=`; + const urlLine = `MESH_BASE_URL=${meshUrl}`; + const envBlock = `${keyLine}\n${urlLine}`; + const handleGenerateKey = async () => { setGenerating(true); try { @@ -225,54 +174,28 @@ function TypegenSectionInner({ virtualMcp }: { virtualMcp: VirtualMCPEntity }) { } }; - const handleCopy = async () => { + const handleCopyCmd = async () => { await navigator.clipboard.writeText(command); - setCopied(true); - toast.success("Command copied to clipboard"); - setTimeout(() => setCopied(false), 2000); + setCopiedCmd(true); + toast.success("Command copied"); + setTimeout(() => setCopiedCmd(false), 2000); }; - return ( -
-
-
-

- Generate typed client -

-

- Introspects this agent and writes a typed{" "} - client.ts you can import - directly. -

-
- {!apiKey && ( - - )} -
+ const handleCopyEnv = async () => { + await navigator.clipboard.writeText(envBlock); + setCopiedEnv(true); + toast.success("Environment variables copied"); + setTimeout(() => setCopiedEnv(false), 2000); + }; + return ( +
{apiKey && (

Store this key securely — it won't be shown again.

)} -

- Generate client -

@@ -283,9 +206,9 @@ function TypegenSectionInner({ virtualMcp }: { virtualMcp: VirtualMCPEntity }) { variant="ghost" size="icon" className="size-6 shrink-0" - onClick={handleCopy} + onClick={handleCopyCmd} > - {copied ? ( + {copiedCmd ? ( ) : ( @@ -294,60 +217,78 @@ function TypegenSectionInner({ virtualMcp }: { virtualMcp: VirtualMCPEntity }) {
-

- Runtime variables -

- -
- ); -} - -function EnvVarsBlock({ apiKey }: { apiKey: string | null }) { - const [copied, setCopied] = useState(false); - const meshUrl = window.location.origin; - const keyLine = apiKey ? `MESH_API_KEY=${apiKey}` : `MESH_API_KEY=`; - const urlLine = `MESH_BASE_URL=${meshUrl}`; - const envBlock = `${keyLine}\n${urlLine}`; - - const handleCopy = async () => { - await navigator.clipboard.writeText(envBlock); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; +
+
+ + {keyLine} +
+ {urlLine} +
+ +
+
- return ( -
-
- - {keyLine} -
- {urlLine} -
+ {!apiKey && ( -
+ )}
); } function TypegenSection({ virtualMcp }: { virtualMcp: VirtualMCPEntity }) { + const [open, setOpen] = useState(false); return ( - } - > - - +
+ + {open && ( +
+ + } + > + + +
+ )} +
); } @@ -363,9 +304,9 @@ export function VirtualMCPShareModal({ onOpenChange: (open: boolean) => void; virtualMcp: VirtualMCPEntity; }) { - const [mode, setMode] = useState< - "passthrough" | "smart_tool_selection" | "code_execution" - >("code_execution"); + const [mode, setMode] = useState("code_execution"); + const [copiedUrl, setCopiedUrl] = useState(false); + const [copiedClaude, setCopiedClaude] = useState(false); const handleModeChange = (value: string) => { if ( @@ -377,26 +318,61 @@ export function VirtualMCPShareModal({ } }; - // Build URL with mode query parameter - // Virtual MCPs (agents) are accessed via the virtual-mcp endpoint const virtualMcpUrl = new URL( `/mcp/virtual-mcp/${virtualMcp.id}`, window.location.origin, ); virtualMcpUrl.searchParams.set("mode", mode); - // Server name for IDE integrations const serverName = virtualMcp.title || `agent-${virtualMcp.id?.slice(0, 8) ?? "default"}`; + const handleCopyUrl = async () => { + await navigator.clipboard.writeText(virtualMcpUrl.href); + setCopiedUrl(true); + toast.success("Agent URL copied to clipboard"); + setTimeout(() => setCopiedUrl(false), 2000); + }; + + const handleInstallCursor = () => { + const slugifiedServerName = slugify(serverName); + const connectionConfig = { + type: "http", + url: virtualMcpUrl.href, + headers: { "x-mesh-client": "Cursor" }, + }; + const base64Config = utf8ToBase64( + JSON.stringify(connectionConfig, null, 2), + ); + const deeplink = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(slugifiedServerName)}&config=${encodeURIComponent(base64Config)}`; + window.open(deeplink, "_blank"); + toast.success("Opening Cursor..."); + }; + + const handleInstallClaude = async () => { + const slugifiedServerName = slugify(serverName); + const connectionConfig = { + type: "http", + url: virtualMcpUrl.href, + headers: { "x-mesh-client": "Claude Code" }, + }; + const configJson = JSON.stringify(connectionConfig, null, 2); + const command = `claude mcp add-json "${slugifiedServerName}" '${configJson.replace(/'/g, "'\\''")}'`; + await navigator.clipboard.writeText(command); + setCopiedClaude(true); + toast.success("Claude Code command copied to clipboard"); + setTimeout(() => setCopiedClaude(false), 2000); + }; + return ( Connect +
- {/* Mode Selection */} + {/* Mode Selection — original radio card layout */}

@@ -406,12 +382,17 @@ export function VirtualMCPShareModal({ {/* Passthrough Option */}