Skip to content
Closed
25 changes: 22 additions & 3 deletions apps/mesh/src/shared/utils/group-connections.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import type { ConnectionEntity } from "@decocms/mesh-sdk";
import { getConnectionSlug } from "./connection-slug";

/**
* Returns the canonical display title for a connection in catalog/card/header contexts.
* For registry-installed connections (those with app_name set), the canonical name is
* derived from the stable app_name slug so that user-renamed instances don't pollute
* the group title. For custom connections (no app_name), the user-set title is used.
*
* Use the raw connection.title only when showing the specific instance matters
* (e.g., the instance list inside a connection detail, or the binding selector).
*/
export function getConnectionDisplayTitle(
connection: ConnectionEntity,
): string {
if (connection.app_name) {
// Convert slug → display title: "google-gmail" → "Google Gmail"
// Strip optional scope prefix like "@scope/name" first.
const slug = connection.app_name.replace(/^@[^/]+\//, "");
return slug.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
return connection.title;
}

export interface ConnectionGroup {
type: "group";
key: string;
Expand Down Expand Up @@ -47,9 +68,7 @@ export function groupConnections(
type: "group",
key,
icon: first.icon,
title: first.app_name
? first.title.replace(/\s*\(\d+\)\s*$/, "")
: first.title,
title: getConnectionDisplayTitle(first),
connections: bucket,
});
}
Expand Down
17 changes: 4 additions & 13 deletions apps/mesh/src/web/components/details/connection/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -424,12 +425,7 @@ function ConnectionInspectorViewWithConnection({
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
{(() => {
const first = siblings[0] ?? connection;
return first.app_name
? first.title.replace(/\s*\(\d+\)\s*$/, "")
: first.title;
})()}
{getConnectionDisplayTitle(siblings[0] ?? connection)}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
Expand Down Expand Up @@ -562,12 +558,7 @@ function ConnectionInspectorViewWithConnection({
<div className="flex flex-col h-full overflow-hidden">
<ConnectionDetailHeader
connection={connection}
displayTitle={(() => {
const first = siblings[0] ?? connection;
return first.app_name
? first.title.replace(/\s*\(\d+\)\s*$/, "")
: first.title;
})()}
displayTitle={getConnectionDisplayTitle(siblings[0] ?? connection)}
/>
<div className="flex-1 overflow-auto @container">
<div className="grid grid-cols-1 @3xl:grid-cols-2 gap-5 p-6">
Expand All @@ -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);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
const nextNumber = siblings.length + 1;
const newTitle = `${baseName} (${nextNumber})`;
const newId = generatePrefixedId("conn");
Expand Down
9 changes: 8 additions & 1 deletion apps/mesh/src/web/routes/orgs/connections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ import {

import {
groupConnections,
getConnectionDisplayTitle,
type ConnectionGroup,
} from "@/shared/utils/group-connections";

Expand Down Expand Up @@ -552,6 +553,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,
Expand Down Expand Up @@ -1054,7 +1058,10 @@ function ConnectionResults({
return (
<ConnectionCard
key={connection.id}
connection={connection}
connection={{
...connection,
title: getConnectionDisplayTitle(connection),
}}
fallbackIcon={<Container />}
onClick={() =>
selectionMode
Expand Down
12 changes: 9 additions & 3 deletions apps/mesh/src/web/views/virtual-mcp/add-connection-dialog.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -296,6 +299,9 @@ function AddConnectionDialogContent({
const icon =
item.server?.icons?.[0]?.src ||
getGitHubAvatarUrl(item.server?.repository) ||
item.icon ||
item.image ||
item.logo ||
null;

return (
Expand Down Expand Up @@ -373,7 +379,7 @@ function AddConnectionDialogContent({
const c = item.connection;
return renderConnectedApp(
c.id,
c.title,
getConnectionDisplayTitle(c),
c.icon,
c.description ?? null,
[c],
Expand Down Expand Up @@ -471,7 +477,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({
Expand Down
3 changes: 2 additions & 1 deletion apps/mesh/src/web/views/virtual-mcp/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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)})`;
Expand Down
Loading