diff --git a/apps/mesh/src/shared/utils/group-connections.ts b/apps/mesh/src/shared/utils/group-connections.ts index d222a46fdf..1e577edaca 100644 --- a/apps/mesh/src/shared/utils/group-connections.ts +++ b/apps/mesh/src/shared/utils/group-connections.ts @@ -1,6 +1,27 @@ import type { ConnectionEntity } from "@decocms/mesh-sdk"; import { getConnectionSlug } from "./connection-slug"; +/** + * Strip auto-generated instance suffixes like "(2)" or "(a1b2)" from a title. + * Matches 1-6 character parenthesized suffixes at the end of the string. + */ +const INSTANCE_SUFFIX_RE = /\s*\([^)]{1,6}\)\s*$/; + +/** + * Returns the canonical display title for a connection. + * Checks metadata.displayName first (set at install time, never changes), + * then falls back to stripping instance suffixes from the title. + */ +export function getConnectionDisplayTitle( + connection: ConnectionEntity, +): string { + const metadata = connection.metadata as Record | null; + if (metadata?.displayName && typeof metadata.displayName === "string") { + return metadata.displayName; + } + return connection.title.replace(INSTANCE_SUFFIX_RE, ""); +} + export interface ConnectionGroup { type: "group"; key: string; @@ -16,8 +37,14 @@ export interface SingleConnection { export type GroupedItem = SingleConnection | ConnectionGroup; +/** + * Groups connections by their slug (app_name or derived from URL/title). + * Accepts an optional registry title lookup so group/single cards show + * the canonical registry name instead of the (possibly renamed) instance title. + */ export function groupConnections( connections: ConnectionEntity[], + registryTitles?: Map, ): GroupedItem[] { const buckets = new Map(); for (const c of connections) { @@ -40,6 +67,8 @@ export function groupConnections( const bucket = buckets.get(key)!; const first = bucket[0]!; + const registryTitle = first.app_name && registryTitles?.get(first.app_name); + if (bucket.length === 1) { items.push({ type: "single", connection: first }); } else { @@ -47,9 +76,7 @@ export function groupConnections( type: "group", key, icon: first.icon, - title: first.app_name - ? first.title.replace(/\s*\(\d+\)\s*$/, "") - : first.title, + title: registryTitle || getConnectionDisplayTitle(first), connections: bucket, }); } diff --git a/apps/mesh/src/web/components/chat/input.tsx b/apps/mesh/src/web/components/chat/input.tsx index b2c1943867..ba00147bbb 100644 --- a/apps/mesh/src/web/components/chat/input.tsx +++ b/apps/mesh/src/web/components/chat/input.tsx @@ -11,8 +11,10 @@ import { cn } from "@deco/ui/lib/utils.ts"; import { getWellKnownDecopilotVirtualMCP, isDecopilot, + useConnections, useProjectContext, } from "@decocms/mesh-sdk"; + import { useNavigateToAgent } from "@/web/hooks/use-navigate-to-agent"; import { ArrowUp, @@ -56,6 +58,26 @@ import { question004Sound } from "@deco/ui/lib/question-004.ts"; import { AddConnectionDialog } from "@/web/views/virtual-mcp/add-connection-dialog"; import { ConnectionsBanner } from "./connections-banner"; +function HomeConnectionsDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const existingConnections = useConnections(); + const existingConnectionIds = new Set(existingConnections.map((c) => c.id)); + return ( + onOpenChange(false)} + defaultTab="all" + /> + ); +} + // ============================================================================ // VirtualMCPBadge - Internal component for displaying selected virtual MCP // ============================================================================ @@ -648,13 +670,12 @@ export function ChatInput({ - {}} - defaultTab="all" - /> + {showConnectionsBanner && ( + + )} ); } diff --git a/apps/mesh/src/web/components/details/connection/index.tsx b/apps/mesh/src/web/components/details/connection/index.tsx index 481cb5cc7f..ddcf6e7346 100644 --- a/apps/mesh/src/web/components/details/connection/index.tsx +++ b/apps/mesh/src/web/components/details/connection/index.tsx @@ -1,4 +1,5 @@ import { generatePrefixedId } from "@/shared/utils/generate-id"; +import { getConnectionDisplayTitle } from "@/shared/utils/group-connections"; import { EmptyState } from "@/web/components/empty-state.tsx"; import { ErrorBoundary } from "@/web/components/error-boundary"; import { recordToEnvVars } from "@/web/components/env-vars-editor"; @@ -424,12 +425,7 @@ function ConnectionInspectorViewWithConnection({ - {(() => { - const first = siblings[0] ?? connection; - return first.app_name - ? first.title.replace(/\s*\(\d+\)\s*$/, "") - : first.title; - })()} + {getConnectionDisplayTitle(siblings[0] ?? connection)} @@ -562,12 +558,7 @@ function ConnectionInspectorViewWithConnection({
{ - const first = siblings[0] ?? connection; - return first.app_name - ? first.title.replace(/\s*\(\d+\)\s*$/, "") - : first.title; - })()} + displayTitle={getConnectionDisplayTitle(siblings[0] ?? connection)} />
@@ -585,7 +576,7 @@ function ConnectionInspectorViewWithConnection({ setIsAddingInstance(true); try { const base = siblings[0] ?? connection; - const baseName = base.title.replace(/\s*\(\d+\)\s*$/, ""); + const baseName = getConnectionDisplayTitle(base); const nextNumber = siblings.length + 1; const newTitle = `${baseName} (${nextNumber})`; const newId = generatePrefixedId("conn"); @@ -606,7 +597,7 @@ function ConnectionInspectorViewWithConnection({ connection_headers: base.connection_headers ?? null, oauth_config: null, configuration_state: base.configuration_state ?? null, - metadata: null, + metadata: base.metadata ?? null, tools: null, bindings: null, status: "inactive", diff --git a/apps/mesh/src/web/routes/orgs/connections.tsx b/apps/mesh/src/web/routes/orgs/connections.tsx index 46f74cff5b..11c1529b7d 100644 --- a/apps/mesh/src/web/routes/orgs/connections.tsx +++ b/apps/mesh/src/web/routes/orgs/connections.tsx @@ -113,6 +113,7 @@ import type { } from "@/tools/connection/schema"; import { EnvVarsEditor } from "@/web/components/env-vars-editor"; import { + buildRegistryTitleMap, extractConnectionData, getRegistryItemAppName, } from "@/web/utils/extract-connection-data"; @@ -136,6 +137,7 @@ import { import { groupConnections, + getConnectionDisplayTitle, type ConnectionGroup, } from "@/shared/utils/group-connections"; @@ -552,6 +554,9 @@ function CatalogItemCard({ const icon = item.server?.icons?.[0]?.src || getGitHubAvatarUrl(item.server?.repository) || + item.icon || + item.image || + item.logo || null; const appInstances = allConnections.filter( (c) => c.connection_type !== "VIRTUAL" && c.app_name === appName, @@ -695,8 +700,6 @@ function ConnectionResults({ return true; }); - const grouped = groupConnections(filteredConnections); - const toggleSelect = (id: string) => { setSelectedIds((prev) => { const next = new Set(prev); @@ -716,6 +719,8 @@ function ConnectionResults({ listState.searchTerm, ); const registryItems = mergedDiscovery.items; + const registryTitles = buildRegistryTitleMap(registryItems); + const grouped = groupConnections(filteredConnections, registryTitles); const catalogSentinelRef = useInfiniteScroll( mergedDiscovery.loadMore, @@ -1054,7 +1059,13 @@ function ConnectionResults({ return ( } onClick={() => selectionMode diff --git a/apps/mesh/src/web/utils/extract-connection-data.ts b/apps/mesh/src/web/utils/extract-connection-data.ts index 693a260f5f..b88f578382 100644 --- a/apps/mesh/src/web/utils/extract-connection-data.ts +++ b/apps/mesh/src/web/utils/extract-connection-data.ts @@ -25,6 +25,34 @@ export function getRegistryItemAppName( return meshMeta?.appName || item.server?.name || null; } +/** + * Build a map from app_name → display title from a list of registry items. + * Used so connected cards can show the same title as the store catalog. + */ +export function buildRegistryTitleMap( + items: Pick[], +): Map { + const map = new Map(); + for (const item of items) { + const appName = getRegistryItemAppName(item); + if (!appName || map.has(appName)) continue; + const meshMeta = item._meta?.["mcp.mesh"] as + | Record + | undefined; + const title = + meshMeta?.friendlyName || + meshMeta?.friendly_name || + item.server?.title || + item.title || + item.server?.name || + item.name || + item.id || + ""; + if (title) map.set(appName, title); + } + return map; +} + /** * Get a display name for a remote endpoint * Uses the hostname (without common suffixes) as the display name @@ -204,6 +232,7 @@ export function extractConnectionData( configuration_scopes: configScopes ?? null, metadata: { source: "store", + displayName: baseName, registry_item_id: item.id, verified: meshMeta?.verified ?? false, scopeName: meshMeta?.scopeName ?? null, diff --git a/apps/mesh/src/web/views/virtual-mcp/add-connection-dialog.tsx b/apps/mesh/src/web/views/virtual-mcp/add-connection-dialog.tsx index 2b1a0e26dd..839b00e847 100644 --- a/apps/mesh/src/web/views/virtual-mcp/add-connection-dialog.tsx +++ b/apps/mesh/src/web/views/virtual-mcp/add-connection-dialog.tsx @@ -1,4 +1,7 @@ -import { groupConnections } from "@/shared/utils/group-connections"; +import { + groupConnections, + getConnectionDisplayTitle, +} from "@/shared/utils/group-connections"; import { CollectionSearch } from "@/web/components/collections/collection-search.tsx"; import { CollectionTabs } from "@/web/components/collections/collection-tabs.tsx"; import { CreateConnectionDialog } from "@/web/components/connections/create-connection-dialog.tsx"; @@ -14,6 +17,7 @@ import { import { KEYS } from "@/web/lib/query-keys"; import { authClient } from "@/web/lib/auth-client"; import { + buildRegistryTitleMap, extractConnectionData, getRegistryItemAppName, } from "@/web/utils/extract-connection-data"; @@ -176,7 +180,6 @@ function AddConnectionDialogContent({ connectionsData?.pages.flatMap( (p: CollectionListOutput) => p?.items ?? [], ) ?? []; - const grouped = groupConnections(allConnections); // Build set of connected app names to deduplicate catalog items const connectedAppNames = new Set( @@ -190,6 +193,9 @@ function AddConnectionDialogContent({ deferredSearch, ); + const registryTitles = buildRegistryTitleMap(mergedDiscovery.items); + const grouped = groupConnections(allConnections, registryTitles); + const catalogSentinelRef = useInfiniteScroll( mergedDiscovery.loadMore, mergedDiscovery.hasMore, @@ -296,6 +302,9 @@ function AddConnectionDialogContent({ const icon = item.server?.icons?.[0]?.src || getGitHubAvatarUrl(item.server?.repository) || + item.icon || + item.image || + item.logo || null; return ( @@ -373,7 +382,8 @@ function AddConnectionDialogContent({ const c = item.connection; return renderConnectedApp( c.id, - c.title, + (c.app_name && registryTitles.get(c.app_name)) || + getConnectionDisplayTitle(c), c.icon, c.description ?? null, [c], @@ -471,7 +481,7 @@ export function AddConnectionDialog({ const handleCloneAndAdd = async (base: ConnectionEntity) => { setConnectingItemId(base.app_name ?? base.id); try { - const baseName = base.title.replace(/\s*\(\d+\)\s*$/, ""); + const baseName = getConnectionDisplayTitle(base); const newTitle = `${baseName} (${Date.now().toString(36).slice(-4)})`; const created = await connectionActions.create.mutateAsync({ @@ -484,6 +494,7 @@ export function AddConnectionDialog({ app_name: base.app_name ?? null, app_id: base.app_id ?? null, connection_headers: base.connection_headers ?? null, + metadata: base.metadata ?? null, }); const id = created.id; diff --git a/apps/mesh/src/web/views/virtual-mcp/index.tsx b/apps/mesh/src/web/views/virtual-mcp/index.tsx index 028cf77ddd..ac73f3a4e5 100644 --- a/apps/mesh/src/web/views/virtual-mcp/index.tsx +++ b/apps/mesh/src/web/views/virtual-mcp/index.tsx @@ -1,4 +1,5 @@ import { generatePrefixedId } from "@/shared/utils/generate-id"; +import { getConnectionDisplayTitle } from "@/shared/utils/group-connections"; import type { VirtualMCPEntity } from "@/tools/virtual/schema"; import { getUIResourceUri } from "@/mcp-apps/types.ts"; import { useChatTask } from "@/web/components/chat/context"; @@ -1185,7 +1186,7 @@ function VirtualMcpDetailViewWithData({ }; if (!base) return; - const baseName = base.title.replace(/\s*\(.*?\)\s*$/, ""); + const baseName = getConnectionDisplayTitle(base); const newId = generatePrefixedId("conn"); // Temporary title — will be updated with email suffix after OAuth if available const tempTitle = `${baseName} (${Date.now().toString(36).slice(-4)})`; @@ -1201,6 +1202,7 @@ function VirtualMcpDetailViewWithData({ app_name: base.app_name ?? null, app_id: base.app_id ?? null, connection_headers: base.connection_headers ?? null, + metadata: base.metadata ?? null, }); // Handle OAuth if needed