diff --git a/ui/public/brands/apps/airtable.svg b/ui/public/brands/apps/airtable.svg new file mode 100644 index 00000000000..90b5fda6945 --- /dev/null +++ b/ui/public/brands/apps/airtable.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/anthropic-dark.svg b/ui/public/brands/apps/anthropic-dark.svg new file mode 100644 index 00000000000..ab29cdd45eb --- /dev/null +++ b/ui/public/brands/apps/anthropic-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/anthropic.svg b/ui/public/brands/apps/anthropic.svg new file mode 100644 index 00000000000..1005c507d5c --- /dev/null +++ b/ui/public/brands/apps/anthropic.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/api-key-generic.svg b/ui/public/brands/apps/api-key-generic.svg new file mode 100644 index 00000000000..a45770d032a --- /dev/null +++ b/ui/public/brands/apps/api-key-generic.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/asana.svg b/ui/public/brands/apps/asana.svg new file mode 100644 index 00000000000..00986826191 --- /dev/null +++ b/ui/public/brands/apps/asana.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/beehiiv.png b/ui/public/brands/apps/beehiiv.png new file mode 100644 index 00000000000..e0cc05ddc8c Binary files /dev/null and b/ui/public/brands/apps/beehiiv.png differ diff --git a/ui/public/brands/apps/bitly.svg b/ui/public/brands/apps/bitly.svg new file mode 100644 index 00000000000..e1874adf738 --- /dev/null +++ b/ui/public/brands/apps/bitly.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/box.svg b/ui/public/brands/apps/box.svg new file mode 100644 index 00000000000..6dcd67bc0ce --- /dev/null +++ b/ui/public/brands/apps/box.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/brex-dark.svg b/ui/public/brands/apps/brex-dark.svg new file mode 100644 index 00000000000..0a022276214 --- /dev/null +++ b/ui/public/brands/apps/brex-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/brex.svg b/ui/public/brands/apps/brex.svg new file mode 100644 index 00000000000..81c87e4417a --- /dev/null +++ b/ui/public/brands/apps/brex.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/candid.png b/ui/public/brands/apps/candid.png new file mode 100644 index 00000000000..496d5363a17 Binary files /dev/null and b/ui/public/brands/apps/candid.png differ diff --git a/ui/public/brands/apps/clickhouse.svg b/ui/public/brands/apps/clickhouse.svg new file mode 100644 index 00000000000..c7482e7d266 --- /dev/null +++ b/ui/public/brands/apps/clickhouse.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/cloudflare.svg b/ui/public/brands/apps/cloudflare.svg new file mode 100644 index 00000000000..529f2825e7d --- /dev/null +++ b/ui/public/brands/apps/cloudflare.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/cloudinary-dark.svg b/ui/public/brands/apps/cloudinary-dark.svg new file mode 100644 index 00000000000..eeea7a390e6 --- /dev/null +++ b/ui/public/brands/apps/cloudinary-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/cloudinary.svg b/ui/public/brands/apps/cloudinary.svg new file mode 100644 index 00000000000..39ec51487fb --- /dev/null +++ b/ui/public/brands/apps/cloudinary.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/coda.svg b/ui/public/brands/apps/coda.svg new file mode 100644 index 00000000000..b5c2589b986 --- /dev/null +++ b/ui/public/brands/apps/coda.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/composio-dark.svg b/ui/public/brands/apps/composio-dark.svg new file mode 100644 index 00000000000..e3c8f00fc22 --- /dev/null +++ b/ui/public/brands/apps/composio-dark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui/public/brands/apps/composio.svg b/ui/public/brands/apps/composio.svg new file mode 100644 index 00000000000..b87146ce636 --- /dev/null +++ b/ui/public/brands/apps/composio.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ui/public/brands/apps/context7-dark.svg b/ui/public/brands/apps/context7-dark.svg new file mode 100644 index 00000000000..75c17baaeab --- /dev/null +++ b/ui/public/brands/apps/context7-dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/public/brands/apps/context7.svg b/ui/public/brands/apps/context7.svg new file mode 100644 index 00000000000..6d3ab948df7 --- /dev/null +++ b/ui/public/brands/apps/context7.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ui/public/brands/apps/egnyte.svg b/ui/public/brands/apps/egnyte.svg new file mode 100644 index 00000000000..d4e76a40700 --- /dev/null +++ b/ui/public/brands/apps/egnyte.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/embat.svg b/ui/public/brands/apps/embat.svg new file mode 100644 index 00000000000..9b7bd79b96a --- /dev/null +++ b/ui/public/brands/apps/embat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/brands/apps/github-dark.svg b/ui/public/brands/apps/github-dark.svg new file mode 100644 index 00000000000..2de8618d6f3 --- /dev/null +++ b/ui/public/brands/apps/github-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/github.svg b/ui/public/brands/apps/github.svg new file mode 100644 index 00000000000..437afbdfae7 --- /dev/null +++ b/ui/public/brands/apps/github.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/gmail.svg b/ui/public/brands/apps/gmail.svg new file mode 100644 index 00000000000..64649f7fdbe --- /dev/null +++ b/ui/public/brands/apps/gmail.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/google-calendar.svg b/ui/public/brands/apps/google-calendar.svg new file mode 100644 index 00000000000..40e1c882acb --- /dev/null +++ b/ui/public/brands/apps/google-calendar.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/google-chat.svg b/ui/public/brands/apps/google-chat.svg new file mode 100644 index 00000000000..fab3734f8dc --- /dev/null +++ b/ui/public/brands/apps/google-chat.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/google-docs.svg b/ui/public/brands/apps/google-docs.svg new file mode 100644 index 00000000000..1f387c3f117 --- /dev/null +++ b/ui/public/brands/apps/google-docs.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/google-drive.svg b/ui/public/brands/apps/google-drive.svg new file mode 100644 index 00000000000..ee7efc43acb --- /dev/null +++ b/ui/public/brands/apps/google-drive.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/google-people.svg b/ui/public/brands/apps/google-people.svg new file mode 100644 index 00000000000..78dbb3b3713 --- /dev/null +++ b/ui/public/brands/apps/google-people.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/google-sheets.svg b/ui/public/brands/apps/google-sheets.svg new file mode 100644 index 00000000000..23eb2c183bf --- /dev/null +++ b/ui/public/brands/apps/google-sheets.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/google-slides.svg b/ui/public/brands/apps/google-slides.svg new file mode 100644 index 00000000000..3938167c286 --- /dev/null +++ b/ui/public/brands/apps/google-slides.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/google-workspace-search.svg b/ui/public/brands/apps/google-workspace-search.svg new file mode 100644 index 00000000000..c085f1d3702 --- /dev/null +++ b/ui/public/brands/apps/google-workspace-search.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/hugging-face.svg b/ui/public/brands/apps/hugging-face.svg new file mode 100644 index 00000000000..cfd184ec0eb --- /dev/null +++ b/ui/public/brands/apps/hugging-face.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/jira-dark.svg b/ui/public/brands/apps/jira-dark.svg new file mode 100644 index 00000000000..35d5f3b627f --- /dev/null +++ b/ui/public/brands/apps/jira-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/jira.svg b/ui/public/brands/apps/jira.svg new file mode 100644 index 00000000000..ae1f9fb153c --- /dev/null +++ b/ui/public/brands/apps/jira.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/kernel.svg b/ui/public/brands/apps/kernel.svg new file mode 100644 index 00000000000..41188c4c1ac --- /dev/null +++ b/ui/public/brands/apps/kernel.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/public/brands/apps/linear.svg b/ui/public/brands/apps/linear.svg new file mode 100644 index 00000000000..0cf3aa7a44f --- /dev/null +++ b/ui/public/brands/apps/linear.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/local-falcon.png b/ui/public/brands/apps/local-falcon.png new file mode 100644 index 00000000000..293e58272e5 Binary files /dev/null and b/ui/public/brands/apps/local-falcon.png differ diff --git a/ui/public/brands/apps/make-dark.svg b/ui/public/brands/apps/make-dark.svg new file mode 100644 index 00000000000..4182a174f61 --- /dev/null +++ b/ui/public/brands/apps/make-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/make.svg b/ui/public/brands/apps/make.svg new file mode 100644 index 00000000000..b3e20989efd --- /dev/null +++ b/ui/public/brands/apps/make.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/manifest.json b/ui/public/brands/apps/manifest.json new file mode 100644 index 00000000000..9d2a6adc864 --- /dev/null +++ b/ui/public/brands/apps/manifest.json @@ -0,0 +1,598 @@ +{ + "schemaVersion": 1, + "verifiedAt": "2026-08-26", + "simpleIconsVersion": "16.28.0", + "providers": [ + { + "slug": "airtable", + "provider": "Airtable", + "catalogVisible": true, + "localAsset": "/brands/apps/airtable.svg", + "officialSourceUrl": "https://airtable.com/newsroom", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/airtable.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "anthropic", + "provider": "Anthropic", + "catalogVisible": false, + "localAsset": "/brands/apps/anthropic.svg", + "darkAsset": "/brands/apps/anthropic-dark.svg", + "officialSourceUrl": "https://www.anthropic.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/anthropic.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "asana", + "provider": "Asana", + "catalogVisible": true, + "localAsset": "/brands/apps/asana.svg", + "officialSourceUrl": "https://asana.com/brand", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/asana.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "beehiiv", + "provider": "beehiiv", + "catalogVisible": false, + "localAsset": "/brands/apps/beehiiv.png", + "officialSourceUrl": "https://www.beehiiv.com", + "upstreamAssetUrl": "https://beehiiv-marketing-images.s3.amazonaws.com/Redesign2023/favicon.png", + "assetType": "png", + "darkVariantRequired": false + }, + { + "slug": "bitly", + "provider": "Bitly", + "catalogVisible": false, + "localAsset": "/brands/apps/bitly.svg", + "officialSourceUrl": "https://bitly.com/pages/press", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/bitly.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "box", + "provider": "Box", + "catalogVisible": true, + "localAsset": "/brands/apps/box.svg", + "officialSourceUrl": "https://www.box.com/en-gb/about-us/press", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/box.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "brex", + "provider": "Brex", + "catalogVisible": false, + "localAsset": "/brands/apps/brex.svg", + "darkAsset": "/brands/apps/brex-dark.svg", + "officialSourceUrl": "https://www.brex.com/journal/press", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/brex.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "candid", + "provider": "Candid", + "catalogVisible": false, + "localAsset": "/brands/apps/candid.png", + "officialSourceUrl": "https://candid.org", + "upstreamAssetUrl": "https://cdn.candid.org/images/candid-logo/candid-logo-square.png", + "assetType": "png", + "darkVariantRequired": false + }, + { + "slug": "clickhouse", + "provider": "ClickHouse", + "catalogVisible": true, + "localAsset": "/brands/apps/clickhouse.svg", + "officialSourceUrl": "https://github.com/ClickHouse/ClickHouse/blob/12bd453a43819176d25ecf247033f6cb1af54beb/website/images/logo-clickhouse.svg", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/clickhouse.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "cloudflare", + "provider": "Cloudflare", + "catalogVisible": true, + "localAsset": "/brands/apps/cloudflare.svg", + "officialSourceUrl": "https://www.cloudflare.com/logo/", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/cloudflare.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "cloudinary", + "provider": "Cloudinary", + "catalogVisible": true, + "localAsset": "/brands/apps/cloudinary.svg", + "darkAsset": "/brands/apps/cloudinary-dark.svg", + "officialSourceUrl": "https://cloudinary.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/cloudinary.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "coda", + "provider": "Coda", + "catalogVisible": false, + "localAsset": "/brands/apps/coda.svg", + "officialSourceUrl": "https://coda.io", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/coda.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "composio", + "provider": "Composio", + "catalogVisible": false, + "localAsset": "/brands/apps/composio.svg", + "darkAsset": "/brands/apps/composio-dark.svg", + "officialSourceUrl": "https://composio.dev", + "upstreamAssetUrl": "https://composio.dev/logos/composio-black.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "context7", + "provider": "Context7", + "catalogVisible": false, + "localAsset": "/brands/apps/context7.svg", + "darkAsset": "/brands/apps/context7-dark.svg", + "officialSourceUrl": "https://context7.com", + "upstreamAssetUrl": "https://context7.com/_next/static/media/context7-logo-dark.2bd29d7e.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "egnyte", + "provider": "Egnyte", + "catalogVisible": false, + "localAsset": "/brands/apps/egnyte.svg", + "officialSourceUrl": "https://www.egnyte.com/presskit.html", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/egnyte.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "embat", + "provider": "Embat", + "catalogVisible": false, + "localAsset": "/brands/apps/embat.svg", + "officialSourceUrl": "https://www.embat.io", + "upstreamAssetUrl": "https://www.embat.io/favicon.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "github", + "provider": "GitHub", + "catalogVisible": false, + "localAsset": "/brands/apps/github.svg", + "darkAsset": "/brands/apps/github-dark.svg", + "officialSourceUrl": "https://github.com/logos", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/github.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "gmail", + "provider": "Gmail", + "catalogVisible": true, + "localAsset": "/brands/apps/gmail.svg", + "officialSourceUrl": "https://fonts.gstatic.com/s/i/productlogos/gmail_2020q4/v8/192px.svg", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/gmail.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "google-sheets", + "provider": "Google Sheets", + "catalogVisible": true, + "localAsset": "/brands/apps/google-sheets.svg", + "officialSourceUrl": "https://sheets.google.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/googlesheets.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "google-drive", "provider": "Google Drive", "catalogVisible": true, + "localAsset": "/brands/apps/google-drive.svg", "officialSourceUrl": "https://drive.google.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/googledrive.svg", + "assetType": "svg", "darkVariantRequired": false + }, + { + "slug": "google-docs", "provider": "Google Docs", "catalogVisible": true, + "localAsset": "/brands/apps/google-docs.svg", "officialSourceUrl": "https://docs.google.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/googledocs.svg", + "assetType": "svg", "darkVariantRequired": false + }, + { + "slug": "google-slides", "provider": "Google Slides", "catalogVisible": true, + "localAsset": "/brands/apps/google-slides.svg", "officialSourceUrl": "https://slides.google.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/googleslides.svg", + "assetType": "svg", "darkVariantRequired": false + }, + { + "slug": "google-calendar", "provider": "Google Calendar", "catalogVisible": true, + "localAsset": "/brands/apps/google-calendar.svg", "officialSourceUrl": "https://calendar.google.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/googlecalendar.svg", + "assetType": "svg", "darkVariantRequired": false + }, + { + "slug": "google-chat", "provider": "Google Chat", "catalogVisible": true, + "localAsset": "/brands/apps/google-chat.svg", "officialSourceUrl": "https://chat.google.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/googlechat.svg", + "assetType": "svg", "darkVariantRequired": false + }, + { + "slug": "google-people", "provider": "Google People", "catalogVisible": true, + "localAsset": "/brands/apps/google-people.svg", "officialSourceUrl": "https://contacts.google.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/googlecontacts.svg", + "assetType": "svg", "darkVariantRequired": false + }, + { + "slug": "google-workspace-search", "provider": "Google Workspace Search", "catalogVisible": true, + "localAsset": "/brands/apps/google-workspace-search.svg", "officialSourceUrl": "https://workspace.google.com", + "upstreamAssetUrl": "https://fonts.google.com/icons?selected=Material+Symbols+Outlined:search:FILL@0;wght@400;GRAD@0;opsz@24", + "assetType": "svg", "darkVariantRequired": false + }, + { + "slug": "hugging-face", + "provider": "Hugging Face", + "catalogVisible": true, + "localAsset": "/brands/apps/hugging-face.svg", + "officialSourceUrl": "https://huggingface.co/brand", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/huggingface.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "jira", + "provider": "Jira", + "catalogVisible": true, + "localAsset": "/brands/apps/jira.svg", + "darkAsset": "/brands/apps/jira-dark.svg", + "officialSourceUrl": "https://atlassian.design/resources/logo-library", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/jira.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "kernel", + "provider": "Kernel", + "catalogVisible": false, + "localAsset": "/brands/apps/kernel.svg", + "officialSourceUrl": "https://www.kernel.sh", + "upstreamAssetUrl": "https://www.kernel.sh/rebrand/logos/logo-kernel-square.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "linear", + "provider": "Linear", + "catalogVisible": true, + "localAsset": "/brands/apps/linear.svg", + "officialSourceUrl": "https://linear.app", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/linear.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "local-falcon", + "provider": "Local Falcon", + "catalogVisible": false, + "localAsset": "/brands/apps/local-falcon.png", + "officialSourceUrl": "https://www.localfalcon.com", + "upstreamAssetUrl": "https://www.localfalcon.com/uploads/ico/android-chrome-192x192.png", + "assetType": "png", + "darkVariantRequired": false + }, + { + "slug": "make", + "provider": "Make", + "catalogVisible": false, + "localAsset": "/brands/apps/make.svg", + "darkAsset": "/brands/apps/make-dark.svg", + "officialSourceUrl": "https://www.make.com/en/press", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/make.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "manufact", + "provider": "Manufact", + "catalogVisible": false, + "localAsset": "/brands/apps/manufact.svg", + "officialSourceUrl": "https://manufact.com", + "upstreamAssetUrl": "https://manufact.com/favicon.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "mem0", + "provider": "Mem0", + "catalogVisible": true, + "localAsset": "/brands/apps/mem0.svg", + "officialSourceUrl": "https://mem0.ai", + "upstreamAssetUrl": "https://framerusercontent.com/images/2ys67ADJdvcyGmQnhp8vKWSq8.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "miro", + "provider": "Miro", + "catalogVisible": true, + "localAsset": "/brands/apps/miro.svg", + "darkAsset": "/brands/apps/miro-dark.svg", + "officialSourceUrl": "https://miro.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/miro.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "mixpanel", + "provider": "Mixpanel", + "catalogVisible": true, + "localAsset": "/brands/apps/mixpanel.svg", + "officialSourceUrl": "https://mixpanel.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/mixpanel.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "netlify", + "provider": "Netlify", + "catalogVisible": true, + "localAsset": "/brands/apps/netlify.svg", + "officialSourceUrl": "https://www.netlify.com/press/", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/netlify.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "notion", + "provider": "Notion", + "catalogVisible": true, + "localAsset": "/brands/apps/notion.svg", + "darkAsset": "/brands/apps/notion-dark.svg", + "officialSourceUrl": "https://www.notion.so", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/notion.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "oreilly", + "provider": "O'Reilly", + "catalogVisible": false, + "localAsset": "/brands/apps/oreilly.svg", + "officialSourceUrl": "https://www.oreilly.com/about/logos/", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/oreilly.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "pagerduty", + "provider": "PagerDuty", + "catalogVisible": true, + "localAsset": "/brands/apps/pagerduty.svg", + "officialSourceUrl": "https://www.pagerduty.com/brand/", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/pagerduty.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "planetscale", + "provider": "PlanetScale", + "catalogVisible": false, + "localAsset": "/brands/apps/planetscale.svg", + "darkAsset": "/brands/apps/planetscale-dark.svg", + "officialSourceUrl": "https://planetscale.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/planetscale.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "posthog", + "provider": "PostHog", + "catalogVisible": true, + "localAsset": "/brands/apps/posthog.svg", + "darkAsset": "/brands/apps/posthog-dark.svg", + "officialSourceUrl": "https://posthog.com/handbook/company/brand-assets", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/posthog.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "postman", + "provider": "Postman", + "catalogVisible": true, + "localAsset": "/brands/apps/postman.svg", + "officialSourceUrl": "https://www.getpostman.com/resources/media-assets/", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/postman.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "razorpay", + "provider": "Razorpay", + "catalogVisible": false, + "localAsset": "/brands/apps/razorpay.svg", + "darkAsset": "/brands/apps/razorpay-dark.svg", + "officialSourceUrl": "https://razorpay.com/newsroom/brand-assets/", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/razorpay.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "resend", + "provider": "Resend", + "catalogVisible": true, + "localAsset": "/brands/apps/resend.svg", + "darkAsset": "/brands/apps/resend-dark.svg", + "officialSourceUrl": "https://resend.com/brand", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/resend.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "sanity", + "provider": "Sanity", + "catalogVisible": false, + "localAsset": "/brands/apps/sanity.svg", + "darkAsset": "/brands/apps/sanity-dark.svg", + "officialSourceUrl": "https://github.com/sanity-io/logos/blob/e298a1ac2757c83910fe08b496a115bc9aef06bd/src/sanityMonogram.tsx", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/sanity.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "sentry", + "provider": "Sentry", + "catalogVisible": true, + "localAsset": "/brands/apps/sentry.svg", + "darkAsset": "/brands/apps/sentry-dark.svg", + "officialSourceUrl": "https://sentry.io/branding/", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/sentry.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "shopify", + "provider": "Shopify", + "catalogVisible": true, + "localAsset": "/brands/apps/shopify.svg", + "officialSourceUrl": "https://www.shopify.com/brand-assets", + "upstreamAssetUrl": "https://cdn.shopify.com/static/brand-assets/shopify-shopping-bag.zip", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "similarweb", + "provider": "Similarweb", + "catalogVisible": false, + "localAsset": "/brands/apps/similarweb.svg", + "darkAsset": "/brands/apps/similarweb-dark.svg", + "officialSourceUrl": "https://www.similarweb.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/similarweb.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "slack", + "provider": "Slack", + "catalogVisible": false, + "localAsset": "/brands/apps/slack.png", + "officialSourceUrl": "https://slack.com/brand-guidelines", + "upstreamAssetUrl": "https://a.slack-edge.com/80588/marketing/img/meta/slack_hash_256.png", + "assetType": "png", + "darkVariantRequired": false + }, + { + "slug": "stripe", + "provider": "Stripe", + "catalogVisible": true, + "localAsset": "/brands/apps/stripe.svg", + "officialSourceUrl": "https://stripe.com/newsroom/information", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/stripe.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "supabase", + "provider": "Supabase", + "catalogVisible": true, + "localAsset": "/brands/apps/supabase.svg", + "officialSourceUrl": "https://github.com/supabase/supabase/blob/4031a7549f5d46da7bc79c01d56be4177dc7c114/packages/common/assets/images/supabase-logo-wordmark--light.svg", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/supabase.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "ticket-tailor", + "provider": "Ticket Tailor", + "catalogVisible": false, + "localAsset": "/brands/apps/ticket-tailor.svg", + "darkAsset": "/brands/apps/ticket-tailor-dark.svg", + "officialSourceUrl": "https://www.tickettailor.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/tickettailor.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "ticktick", + "provider": "TickTick", + "catalogVisible": false, + "localAsset": "/brands/apps/ticktick.svg", + "officialSourceUrl": "https://ticktick.com", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/ticktick.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "todoist", + "provider": "Todoist", + "catalogVisible": true, + "localAsset": "/brands/apps/todoist.svg", + "officialSourceUrl": "https://doist.com/press", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/todoist.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "vercel", + "provider": "Vercel", + "catalogVisible": false, + "localAsset": "/brands/apps/vercel.svg", + "darkAsset": "/brands/apps/vercel-dark.svg", + "officialSourceUrl": "https://vercel.com/geist/brands", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/vercel.svg", + "assetType": "svg", + "darkVariantRequired": true + }, + { + "slug": "webflow", + "provider": "Webflow", + "catalogVisible": true, + "localAsset": "/brands/apps/webflow.svg", + "officialSourceUrl": "https://brand-at.webflow.io/resources#logos", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/webflow.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "wix", + "provider": "Wix", + "catalogVisible": true, + "localAsset": "/brands/apps/wix.svg", + "officialSourceUrl": "https://www.wix.com/about/design-assets", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/wix.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "xero", + "provider": "Xero", + "catalogVisible": false, + "localAsset": "/brands/apps/xero.svg", + "officialSourceUrl": "https://www.xero.com/uk/about/media/downloads", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/xero.svg", + "assetType": "svg", + "darkVariantRequired": false + }, + { + "slug": "zapier", + "provider": "Zapier", + "catalogVisible": true, + "localAsset": "/brands/apps/zapier.svg", + "officialSourceUrl": "https://www.figma.com/file/NQFxTCE5pGR3dHZt0DkOyy/Zapier-Brand-Guidelines-%5BExternal%5D?type=design&node-id=101-9701&mode=design", + "upstreamAssetUrl": "https://github.com/simple-icons/simple-icons/blob/16.28.0/icons/zapier.svg", + "assetType": "svg", + "darkVariantRequired": false + } + ] +} diff --git a/ui/public/brands/apps/manufact.svg b/ui/public/brands/apps/manufact.svg new file mode 100644 index 00000000000..9213e86db48 --- /dev/null +++ b/ui/public/brands/apps/manufact.svg @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ui/public/brands/apps/mem0.svg b/ui/public/brands/apps/mem0.svg new file mode 100644 index 00000000000..9a3a9aa247e --- /dev/null +++ b/ui/public/brands/apps/mem0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/brands/apps/miro-dark.svg b/ui/public/brands/apps/miro-dark.svg new file mode 100644 index 00000000000..7d688980793 --- /dev/null +++ b/ui/public/brands/apps/miro-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/miro.svg b/ui/public/brands/apps/miro.svg new file mode 100644 index 00000000000..bbcc06076c8 --- /dev/null +++ b/ui/public/brands/apps/miro.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/mixpanel.svg b/ui/public/brands/apps/mixpanel.svg new file mode 100644 index 00000000000..a844f19d480 --- /dev/null +++ b/ui/public/brands/apps/mixpanel.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/netlify.svg b/ui/public/brands/apps/netlify.svg new file mode 100644 index 00000000000..2712f080a20 --- /dev/null +++ b/ui/public/brands/apps/netlify.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/notion-dark.svg b/ui/public/brands/apps/notion-dark.svg new file mode 100644 index 00000000000..33a78c382f6 --- /dev/null +++ b/ui/public/brands/apps/notion-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/notion.svg b/ui/public/brands/apps/notion.svg new file mode 100644 index 00000000000..89a61bad41c --- /dev/null +++ b/ui/public/brands/apps/notion.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/oauth-generic.svg b/ui/public/brands/apps/oauth-generic.svg new file mode 100644 index 00000000000..169b2f8b9a4 --- /dev/null +++ b/ui/public/brands/apps/oauth-generic.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/oreilly.svg b/ui/public/brands/apps/oreilly.svg new file mode 100644 index 00000000000..63defbc36ea --- /dev/null +++ b/ui/public/brands/apps/oreilly.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/pagerduty.svg b/ui/public/brands/apps/pagerduty.svg new file mode 100644 index 00000000000..b121330ef32 --- /dev/null +++ b/ui/public/brands/apps/pagerduty.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/planetscale-dark.svg b/ui/public/brands/apps/planetscale-dark.svg new file mode 100644 index 00000000000..1d4dd3b81ec --- /dev/null +++ b/ui/public/brands/apps/planetscale-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/planetscale.svg b/ui/public/brands/apps/planetscale.svg new file mode 100644 index 00000000000..b9ca6f28964 --- /dev/null +++ b/ui/public/brands/apps/planetscale.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/posthog-dark.svg b/ui/public/brands/apps/posthog-dark.svg new file mode 100644 index 00000000000..4601419c2be --- /dev/null +++ b/ui/public/brands/apps/posthog-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/posthog.svg b/ui/public/brands/apps/posthog.svg new file mode 100644 index 00000000000..905bebb92bd --- /dev/null +++ b/ui/public/brands/apps/posthog.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/postman.svg b/ui/public/brands/apps/postman.svg new file mode 100644 index 00000000000..4586aec24b9 --- /dev/null +++ b/ui/public/brands/apps/postman.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/razorpay-dark.svg b/ui/public/brands/apps/razorpay-dark.svg new file mode 100644 index 00000000000..3d779a50c8f --- /dev/null +++ b/ui/public/brands/apps/razorpay-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/razorpay.svg b/ui/public/brands/apps/razorpay.svg new file mode 100644 index 00000000000..fa429869025 --- /dev/null +++ b/ui/public/brands/apps/razorpay.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/resend-dark.svg b/ui/public/brands/apps/resend-dark.svg new file mode 100644 index 00000000000..24e46f97418 --- /dev/null +++ b/ui/public/brands/apps/resend-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/resend.svg b/ui/public/brands/apps/resend.svg new file mode 100644 index 00000000000..8e56beafdc0 --- /dev/null +++ b/ui/public/brands/apps/resend.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/sanity-dark.svg b/ui/public/brands/apps/sanity-dark.svg new file mode 100644 index 00000000000..1d2130c06a7 --- /dev/null +++ b/ui/public/brands/apps/sanity-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/sanity.svg b/ui/public/brands/apps/sanity.svg new file mode 100644 index 00000000000..90ebbb84067 --- /dev/null +++ b/ui/public/brands/apps/sanity.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/sentry-dark.svg b/ui/public/brands/apps/sentry-dark.svg new file mode 100644 index 00000000000..b1b01b5ee71 --- /dev/null +++ b/ui/public/brands/apps/sentry-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/sentry.svg b/ui/public/brands/apps/sentry.svg new file mode 100644 index 00000000000..c1089a106e2 --- /dev/null +++ b/ui/public/brands/apps/sentry.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/shopify.svg b/ui/public/brands/apps/shopify.svg new file mode 100644 index 00000000000..93bb6553ee3 --- /dev/null +++ b/ui/public/brands/apps/shopify.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/public/brands/apps/similarweb-dark.svg b/ui/public/brands/apps/similarweb-dark.svg new file mode 100644 index 00000000000..1088266f79f --- /dev/null +++ b/ui/public/brands/apps/similarweb-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/similarweb.svg b/ui/public/brands/apps/similarweb.svg new file mode 100644 index 00000000000..1ad6287c689 --- /dev/null +++ b/ui/public/brands/apps/similarweb.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/slack.png b/ui/public/brands/apps/slack.png new file mode 100644 index 00000000000..a045a159f71 Binary files /dev/null and b/ui/public/brands/apps/slack.png differ diff --git a/ui/public/brands/apps/stripe.svg b/ui/public/brands/apps/stripe.svg new file mode 100644 index 00000000000..61b97007ff2 --- /dev/null +++ b/ui/public/brands/apps/stripe.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/supabase.svg b/ui/public/brands/apps/supabase.svg new file mode 100644 index 00000000000..f3e85e9a435 --- /dev/null +++ b/ui/public/brands/apps/supabase.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/ticket-tailor-dark.svg b/ui/public/brands/apps/ticket-tailor-dark.svg new file mode 100644 index 00000000000..d1ea0a846f9 --- /dev/null +++ b/ui/public/brands/apps/ticket-tailor-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/ticket-tailor.svg b/ui/public/brands/apps/ticket-tailor.svg new file mode 100644 index 00000000000..d89f50105e1 --- /dev/null +++ b/ui/public/brands/apps/ticket-tailor.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/ticktick.svg b/ui/public/brands/apps/ticktick.svg new file mode 100644 index 00000000000..ecb416de19d --- /dev/null +++ b/ui/public/brands/apps/ticktick.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/todoist.svg b/ui/public/brands/apps/todoist.svg new file mode 100644 index 00000000000..79bf5e3c7e6 --- /dev/null +++ b/ui/public/brands/apps/todoist.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/vercel-dark.svg b/ui/public/brands/apps/vercel-dark.svg new file mode 100644 index 00000000000..7db29537ca2 --- /dev/null +++ b/ui/public/brands/apps/vercel-dark.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/vercel.svg b/ui/public/brands/apps/vercel.svg new file mode 100644 index 00000000000..28526d84560 --- /dev/null +++ b/ui/public/brands/apps/vercel.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/webflow.svg b/ui/public/brands/apps/webflow.svg new file mode 100644 index 00000000000..5a49553b0dc --- /dev/null +++ b/ui/public/brands/apps/webflow.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/wix.svg b/ui/public/brands/apps/wix.svg new file mode 100644 index 00000000000..a1f53bce053 --- /dev/null +++ b/ui/public/brands/apps/wix.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/xero.svg b/ui/public/brands/apps/xero.svg new file mode 100644 index 00000000000..54c3620478c --- /dev/null +++ b/ui/public/brands/apps/xero.svg @@ -0,0 +1 @@ + diff --git a/ui/public/brands/apps/zapier.svg b/ui/public/brands/apps/zapier.svg new file mode 100644 index 00000000000..368875a42e7 --- /dev/null +++ b/ui/public/brands/apps/zapier.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/AppConnectionSidebar.test.tsx b/ui/src/components/AppConnectionSidebar.test.tsx index bf3a44a59df..e243a70ddef 100644 --- a/ui/src/components/AppConnectionSidebar.test.tsx +++ b/ui/src/components/AppConnectionSidebar.test.tsx @@ -51,6 +51,27 @@ vi.mock("@/api/tools", () => ({ toolsApi: mockToolsApi, })); +vi.mock("@/pages/apps/AppLogo", () => ({ + AppLogo: ({ + name, + brandKey, + logoUrl, + allowRemoteFallback, + }: { + name: string; + brandKey?: string | null; + logoUrl?: string | null; + allowRemoteFallback?: boolean; + }) => ( + + ), +})); + vi.mock("./SidebarNavItem", () => ({ SidebarNavItem: (props: { to: string; @@ -93,6 +114,7 @@ async function flushReact() { function connection(overrides: Record = {}) { return { id: "conn-1", + applicationId: "app-1", name: "GitHub", transport: "mcp_remote", status: "active", @@ -185,6 +207,63 @@ describe("AppConnectionSidebar", () => { expect(container.querySelector('[data-to="/apps/conn-1/setup"]')?.getAttribute("data-active")).toBe("false"); }); + it("uses the application key for a customized connection display name", async () => { + mockToolsApi.getConnection.mockResolvedValue(connection({ name: "Dotta's GitHub" })); + + await renderSidebar(); + + expect(container.querySelector("[data-app-logo]")?.getAttribute("data-brand-key")).toBe("github"); + expect(container.querySelector("[data-app-logo]")?.getAttribute("data-allow-remote-fallback")).toBe("true"); + }); + + it("keeps remote fallback disabled until the stable application key resolves", async () => { + let resolveApplications!: (value: { + applications: Array<{ id: string; applicationKey: string; name: string; status: string }>; + }) => void; + mockToolsApi.getConnection.mockResolvedValue(connection({ name: "Dotta's source control" })); + mockToolsApi.listApplications.mockReturnValueOnce(new Promise((resolve) => { + resolveApplications = resolve; + })); + + await renderSidebar(); + + expect(container.querySelector("[data-app-logo]")?.getAttribute("data-brand-key")).toBe(""); + expect(container.querySelector("[data-app-logo]")?.getAttribute("data-allow-remote-fallback")).toBe("false"); + + await act(async () => { + resolveApplications({ + applications: [{ id: "app-1", applicationKey: "github", name: "GitHub", status: "active" }], + }); + }); + await flushReact(); + + expect(container.querySelector("[data-app-logo]")?.getAttribute("data-brand-key")).toBe("github"); + expect(container.querySelector("[data-app-logo]")?.getAttribute("data-allow-remote-fallback")).toBe("true"); + }); + + it("allows remote fallback after application identity lookup fails", async () => { + mockToolsApi.listApplications.mockRejectedValueOnce(new Error("Application lookup unavailable")); + + await renderSidebar(); + + expect(container.querySelector("[data-app-logo]")?.getAttribute("data-allow-remote-fallback")).toBe("true"); + }); + + it("keeps a customized connection logo when application identity lookup fails", async () => { + mockToolsApi.getConnection.mockResolvedValue(connection({ + name: "Dotta's source control", + config: { sourceTemplateKey: "github" }, + })); + mockToolsApi.listApplications.mockRejectedValueOnce(new Error("Application lookup unavailable")); + + await renderSidebar(); + + const logo = container.querySelector("[data-app-logo]"); + expect(logo?.getAttribute("data-brand-key")).toBe("github"); + expect(logo?.getAttribute("data-logo-url")).toBe("https://example.test/github.png"); + expect(logo?.getAttribute("data-allow-remote-fallback")).toBe("true"); + }); + it("renders application-mode tabs under the not-connected app route", async () => { currentPath.value = "/apps/app/app-1/review"; diff --git a/ui/src/components/AppConnectionSidebar.tsx b/ui/src/components/AppConnectionSidebar.tsx index dddb6fc9002..5189f7a2ddf 100644 --- a/ui/src/components/AppConnectionSidebar.tsx +++ b/ui/src/components/AppConnectionSidebar.tsx @@ -18,6 +18,8 @@ import { import { isComposioBrokerConnection } from "@/pages/apps/composio-services"; import { AppLogo } from "@/pages/apps/AppLogo"; import { + appApplicationSourceSlug, + appConnectionSourceSlug, appDefinitionLogoUrl, appDefinitionName, appDefinitionSlug, @@ -41,7 +43,7 @@ export function AppDetailSidebar(props: AppDetailSidebarProps) { const applicationsQuery = useQuery({ queryKey: queryKeys.tools.applications(selectedCompanyId ?? "__none__"), queryFn: () => toolsApi.listApplications(selectedCompanyId!), - enabled: props.kind === "application" && !!selectedCompanyId, + enabled: !!selectedCompanyId, }); const connectionsQuery = useQuery({ queryKey: queryKeys.tools.connections(selectedCompanyId ?? "__none__"), @@ -62,9 +64,8 @@ export function AppDetailSidebar(props: AppDetailSidebarProps) { const connection = connectionQuery.data; const isBroker = isComposioBrokerConnection(connection); - const application = props.kind === "application" - ? (applicationsQuery.data?.applications ?? []).find((app) => app.id === props.applicationId) - : null; + const applicationId = props.kind === "application" ? props.applicationId : connection?.applicationId; + const application = (applicationsQuery.data?.applications ?? []).find((app) => app.id === applicationId) ?? null; const appConnections = props.kind === "application" ? (connectionsQuery.data?.connections ?? []).filter((candidate) => candidate.applicationId === props.applicationId) : []; @@ -75,6 +76,9 @@ export function AppDetailSidebar(props: AppDetailSidebarProps) { connection, application ?? undefined, ); + const brandKey = appApplicationSourceSlug(application) + ?? appConnectionSourceSlug(connection) + ?? (logoEntry ? appDefinitionSlug(logoEntry) : null); const reviewConnectionId = connection?.id ?? previousConnection?.id ?? null; const attentionItem = reviewConnectionId ? attentionQuery.data?.apps.find((app) => app.connection.id === reviewConnectionId) @@ -97,7 +101,13 @@ export function AppDetailSidebar(props: AppDetailSidebarProps) { All apps
- + {appName}
@@ -142,8 +152,9 @@ function galleryEntryFor( connection: ToolConnection | undefined, application: ToolApplication | undefined, ): AppGalleryDisplayEntry | null { - if (application?.applicationKey) { - const keyed = apps.find((app) => appDefinitionSlug(app) === application.applicationKey); + const sourceSlug = appApplicationSourceSlug(application) ?? appConnectionSourceSlug(connection); + if (sourceSlug) { + const keyed = apps.find((app) => appDefinitionSlug(app) === sourceSlug); if (keyed) return keyed; } const name = (connection?.name ?? application?.name)?.toLowerCase(); diff --git a/ui/src/lib/app-brand-assets.test.ts b/ui/src/lib/app-brand-assets.test.ts new file mode 100644 index 00000000000..0944ed54e98 --- /dev/null +++ b/ui/src/lib/app-brand-assets.test.ts @@ -0,0 +1,52 @@ +import { existsSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +type BrandProvider = { + slug: string; + localAsset: string; + darkAsset?: string; + assetType: "svg" | "png"; + darkVariantRequired: boolean; +}; + +type BrandManifest = { + schemaVersion: number; + providers: BrandProvider[]; +}; + +const uiRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(uiRoot, "public", "brands", "apps", "manifest.json"); +const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as BrandManifest; + +function publicAssetPath(asset: string): string { + expect(asset).toMatch(/^\/brands\/apps\/[a-z0-9-]+\.(svg|png)$/); + return path.join(uiRoot, "public", asset.slice(1)); +} + +describe("local app brand assets", () => { + it("maps every provider to a unique local asset that exists", () => { + expect(manifest.schemaVersion).toBe(1); + expect(manifest.providers.length).toBeGreaterThan(50); + expect(new Set(manifest.providers.map((provider) => provider.slug)).size).toBe( + manifest.providers.length, + ); + + for (const provider of manifest.providers) { + const assetPath = publicAssetPath(provider.localAsset); + expect(existsSync(assetPath), `${provider.slug} local asset should exist`).toBe(true); + expect(statSync(assetPath).isFile(), `${provider.slug} local asset should be a file`).toBe(true); + expect(path.extname(assetPath)).toBe(`.${provider.assetType}`); + } + }); + + it("ships each required dark-theme variant", () => { + for (const provider of manifest.providers.filter((entry) => entry.darkVariantRequired)) { + expect(provider.darkAsset, `${provider.slug} should declare a dark asset`).toBeTruthy(); + const assetPath = publicAssetPath(provider.darkAsset!); + expect(existsSync(assetPath), `${provider.slug} dark asset should exist`).toBe(true); + expect(statSync(assetPath).isFile(), `${provider.slug} dark asset should be a file`).toBe(true); + } + }); +}); diff --git a/ui/src/lib/app-brand-assets.ts b/ui/src/lib/app-brand-assets.ts new file mode 100644 index 00000000000..3e967b64e49 --- /dev/null +++ b/ui/src/lib/app-brand-assets.ts @@ -0,0 +1,74 @@ +export type AppBrandManifestProvider = { + slug: string; + provider: string; + localAsset: string; + darkAsset?: string; +}; + +export type AppBrandManifest = { + schemaVersion: number; + providers: AppBrandManifestProvider[]; +}; + +export type LocalAppBrandAssets = { + light: string; + dark?: string; +}; + +let manifestPromise: Promise | null = null; + +function normalizedProviderKey(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/&/g, "and") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +function isManifest(value: unknown): value is AppBrandManifest { + if (!value || typeof value !== "object") return false; + const candidate = value as Partial; + return candidate.schemaVersion === 1 && Array.isArray(candidate.providers); +} + +async function loadManifest(): Promise { + if (!manifestPromise) { + manifestPromise = fetch("/brands/apps/manifest.json", { credentials: "same-origin" }) + .then(async (response) => { + if (!response.ok) throw new Error(`App brand manifest request failed (${response.status})`); + const value: unknown = await response.json(); + if (!isManifest(value)) throw new Error("App brand manifest is invalid"); + return value; + }) + .catch((error) => { + manifestPromise = null; + throw error; + }); + } + return manifestPromise; +} + +export function resolveLocalAppBrandAssets( + manifest: AppBrandManifest, + providerName: string, +): LocalAppBrandAssets | null { + const key = normalizedProviderKey(providerName); + if (!key) return null; + const provider = manifest.providers.find((candidate) => + candidate.slug === key || normalizedProviderKey(candidate.provider) === key, + ); + if (!provider || typeof provider.localAsset !== "string" || !provider.localAsset) return null; + return { + light: provider.localAsset, + ...(typeof provider.darkAsset === "string" && provider.darkAsset ? { dark: provider.darkAsset } : {}), + }; +} + +export async function loadLocalAppBrandAssets(providerName: string): Promise { + return resolveLocalAppBrandAssets(await loadManifest(), providerName); +} + +export function resetAppBrandManifestCacheForTests() { + manifestPromise = null; +} diff --git a/ui/src/pages/apps/AppDetail.test.tsx b/ui/src/pages/apps/AppDetail.test.tsx index e1d8bebf1ac..b02d113bbe9 100644 --- a/ui/src/pages/apps/AppDetail.test.tsx +++ b/ui/src/pages/apps/AppDetail.test.tsx @@ -10,6 +10,7 @@ import { APP_TABS } from "./app-tabs"; const getConnectionMock = vi.hoisted(() => vi.fn()); const getConnectionInstallsMock = vi.hoisted(() => vi.fn()); +const listApplicationsMock = vi.hoisted(() => vi.fn()); const listGalleryMock = vi.hoisted(() => vi.fn()); const listConnectionsMock = vi.hoisted(() => vi.fn()); const listCatalogMock = vi.hoisted(() => vi.fn()); @@ -40,6 +41,7 @@ vi.mock("@/api/tools", () => ({ toolsApi: { getConnection: (connectionId: string) => getConnectionMock(connectionId), getConnectionInstalls: (connectionId: string) => getConnectionInstallsMock(connectionId), + listApplications: (companyId: string) => listApplicationsMock(companyId), listGallery: (companyId: string) => listGalleryMock(companyId), listConnections: (companyId: string) => listConnectionsMock(companyId), listCatalog: (connectionId: string) => listCatalogMock(connectionId), @@ -76,6 +78,27 @@ vi.mock("@/api/tools", () => ({ }, })); +vi.mock("./AppLogo", () => ({ + AppLogo: ({ + name, + brandKey, + logoUrl, + allowRemoteFallback, + }: { + name: string; + brandKey?: string | null; + logoUrl?: string | null; + allowRemoteFallback?: boolean; + }) => ( + + ), +})); + vi.mock("@/api/access", () => ({ accessApi: { listUserDirectory: (companyId: string) => listUserDirectoryMock(companyId), @@ -267,6 +290,9 @@ describe("AppDetail", () => { mockSearchParams.value = new URLSearchParams(); getConnectionMock.mockResolvedValue(connection()); getConnectionInstallsMock.mockResolvedValue({ connectionId: "conn-1", installs: [] }); + listApplicationsMock.mockResolvedValue({ + applications: [{ id: "app-1", applicationKey: "github", name: "GitHub", status: "active" }], + }); listConnectionsMock.mockResolvedValue({ connections: [] }); listConnectionGrantsMock.mockResolvedValue({ connection: { id: "conn-1", uid: "conn-1" }, @@ -408,6 +434,32 @@ describe("AppDetail", () => { expect(updateConnectionMock).toHaveBeenCalledWith("conn-1", { enabled: false }); }); + it("allows the gallery logo fallback after application identity lookup fails", async () => { + listApplicationsMock.mockRejectedValueOnce(new Error("Application lookup unavailable")); + + await renderAppDetail(); + + expect(container.querySelector("[data-app-logo]")?.getAttribute("data-allow-remote-fallback")).toBe("true"); + }); + + it("keeps a customized connection logo after application identity lookup fails", async () => { + getConnectionMock.mockResolvedValue(connection({ + name: "Dotta's source control", + config: { + url: "https://github.example/mcp", + sourceTemplateKey: "github", + }, + })); + listApplicationsMock.mockRejectedValueOnce(new Error("Application lookup unavailable")); + + await renderAppDetail(); + + const logo = container.querySelector("[data-app-logo]"); + expect(logo?.getAttribute("data-brand-key")).toBe("github"); + expect(logo?.getAttribute("data-logo-url")).toBe("https://example.com/github.png"); + expect(logo?.getAttribute("data-allow-remote-fallback")).toBe("true"); + }); + it("keeps the unverified-server marker on URL-only connection details", async () => { getConnectionMock.mockResolvedValue( connection({ diff --git a/ui/src/pages/apps/AppDetail.tsx b/ui/src/pages/apps/AppDetail.tsx index d0f569cc26d..ab5c7b25d1a 100644 --- a/ui/src/pages/apps/AppDetail.tsx +++ b/ui/src/pages/apps/AppDetail.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Check, Loader2, Pencil } from "lucide-react"; import type { + ToolApplication, ToolConnection, ToolPolicy, ToolProfileWithDetails, @@ -31,6 +32,8 @@ import { cn } from "@/lib/utils"; import { AppLogo } from "./AppLogo"; import { UnverifiedServerBadge } from "./UnverifiedServerBadge"; import { + appApplicationSourceSlug, + appConnectionSourceSlug, appDefinitionLogoUrl, appDefinitionName, appDefinitionSlug, @@ -85,6 +88,11 @@ export function AppDetail() { queryFn: () => toolsApi.listConnections(selectedCompanyId!), enabled: !!selectedCompanyId && activeTab === "setup", }); + const applicationsQuery = useQuery({ + queryKey: queryKeys.tools.applications(selectedCompanyId ?? "__none__"), + queryFn: () => toolsApi.listApplications(selectedCompanyId!), + enabled: !!selectedCompanyId && !!activeTab, + }); const installsQuery = useQuery({ queryKey: queryKeys.tools.connectionInstalls(connectionId), queryFn: () => toolsApi.getConnectionInstalls(connectionId), @@ -143,15 +151,21 @@ export function AppDetail() { }); const connection = connectionQuery.data; + const application = connection + ? (applicationsQuery.data?.applications ?? []).find((candidate) => candidate.id === connection.applicationId) + : undefined; const composioChildConnectionCount = (connectionsQuery.data?.connections ?? []).filter( (candidate) => candidate.status !== "archived" && candidate.config?.provider === "composio" && candidate.config?.parentConnectionId === connectionId, ).length; const logoEntry = useMemo( - () => galleryEntryFor((galleryQuery.data?.apps ?? []) as AppGalleryDisplayEntry[], connection), - [galleryQuery.data, connection], + () => galleryEntryFor((galleryQuery.data?.apps ?? []) as AppGalleryDisplayEntry[], connection, application), + [galleryQuery.data, connection, application], ); + const brandKey = appApplicationSourceSlug(application) + ?? appConnectionSourceSlug(connection) + ?? (logoEntry ? appDefinitionSlug(logoEntry) : null); const userProfileById = useMemo( () => buildCompanyUserProfileMap(userDirectoryQuery.data?.users), [userDirectoryQuery.data], @@ -579,6 +593,8 @@ export function AppDetail() { appName={appName} connection={connection} logoEntry={logoEntry} + brandKey={brandKey} + allowRemoteLogo={!applicationsQuery.isPending} status={status} actionCount={actionCount} owner={owner} @@ -750,6 +766,8 @@ function AppDetailHeader({ appName, connection, logoEntry, + brandKey, + allowRemoteLogo, status, actionCount, owner, @@ -764,6 +782,8 @@ function AppDetailHeader({ appName: string; connection: ToolConnection; logoEntry: AppGalleryDisplayEntry | null; + brandKey: string | null; + allowRemoteLogo: boolean; status: StatusInfo; actionCount: number | null; owner: ConnectionOwnerProfile | null; @@ -781,7 +801,13 @@ function AppDetailHeader({ return (
- +
{renaming ? (
appDefinitionSlug(app) === sourceSlug); + if (keyed) return keyed; + } const name = connection.name.toLowerCase(); return apps.find((app) => appDefinitionName(app).toLowerCase() === name) ?? apps.find((app) => appDefinitionSlug(app) === name) ?? diff --git a/ui/src/pages/apps/AppLogo.brand-assets.test.tsx b/ui/src/pages/apps/AppLogo.brand-assets.test.tsx new file mode 100644 index 00000000000..09ea0f6f899 --- /dev/null +++ b/ui/src/pages/apps/AppLogo.brand-assets.test.tsx @@ -0,0 +1,227 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { resetAppBrandManifestCacheForTests } from "@/lib/app-brand-assets"; +import { AppLogo } from "./AppLogo"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +describe("AppLogo local brand assets", () => { + let container: HTMLDivElement; + let root: ReturnType; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + resetAppBrandManifestCacheForTests(); + }); + + afterEach(() => { + act(() => root?.unmount()); + container.remove(); + vi.unstubAllGlobals(); + }); + + it("loads the manifest and renders its light and dark provider assets", async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + schemaVersion: 1, + providers: [{ + slug: "github", + provider: "GitHub", + localAsset: "/brands/apps/github.svg", + darkAsset: "/brands/apps/github-dark.svg", + }], + }), + }); + vi.stubGlobal("fetch", fetchMock); + root = createRoot(container); + + await act(async () => { + root.render(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(fetchMock).toHaveBeenCalledWith("/brands/apps/manifest.json", { credentials: "same-origin" }); + const images = Array.from(container.querySelectorAll("img")); + expect(images.map((image) => image.getAttribute("src"))).toEqual([ + "/brands/apps/github.svg", + "/brands/apps/github-dark.svg", + ]); + expect(images[0]?.className).toContain("dark:hidden"); + expect(images[1]?.className).toContain("dark:block"); + }); + + it.each([ + { failedIndex: 0, survivingAsset: "/brands/apps/github-dark.svg", survivingClass: "dark:block", fallbackClass: "dark:hidden" }, + { failedIndex: 1, survivingAsset: "/brands/apps/github.svg", survivingClass: "dark:hidden", fallbackClass: "dark:flex" }, + ])("keeps the valid theme asset when image $failedIndex fails", async ({ + failedIndex, + survivingAsset, + survivingClass, + fallbackClass, + }) => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + schemaVersion: 1, + providers: [{ + slug: "github", + provider: "GitHub", + localAsset: "/brands/apps/github.svg", + darkAsset: "/brands/apps/github-dark.svg", + }], + }), + })); + root = createRoot(container); + + await act(async () => { + root.render(); + await Promise.resolve(); + await Promise.resolve(); + }); + const failedImage = container.querySelectorAll("img")[failedIndex]; + expect(failedImage).toBeTruthy(); + + await act(async () => { + failedImage!.dispatchEvent(new Event("error", { bubbles: true })); + }); + + const remainingImages = Array.from(container.querySelectorAll("img")); + expect(remainingImages).toHaveLength(1); + expect(remainingImages[0]?.getAttribute("src")).toBe(survivingAsset); + expect(remainingImages[0]?.className).toContain(survivingClass); + const fallback = container.querySelector("span > span"); + expect(fallback?.textContent).toBe("G"); + expect(fallback?.className).toContain(fallbackClass); + }); + + it("does not request a remote logo while the local manifest lookup is pending", async () => { + let resolveResponse!: (value: { + ok: boolean; + json: () => Promise; + }) => void; + vi.stubGlobal("fetch", vi.fn().mockImplementation(() => new Promise((resolve) => { + resolveResponse = resolve; + }))); + root = createRoot(container); + + act(() => { + root.render(); + }); + + expect(container.querySelector("img")).toBeNull(); + + await act(async () => { + resolveResponse({ + ok: true, + json: async () => ({ + schemaVersion: 1, + providers: [{ + slug: "github", + provider: "GitHub", + localAsset: "/brands/apps/github.svg", + }], + }), + }); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.querySelector("img")?.getAttribute("src")).toBe("/brands/apps/github.svg"); + }); + + it("uses a stable provider key for owner-qualified display names", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + schemaVersion: 1, + providers: [{ + slug: "github", + provider: "GitHub", + localAsset: "/brands/apps/github.svg", + }], + }), + })); + root = createRoot(container); + + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.querySelector("img")?.getAttribute("src")).toBe("/brands/apps/github.svg"); + }); + + it("does not expose a remote logo before a stable provider key arrives", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + schemaVersion: 1, + providers: [{ + slug: "github", + provider: "GitHub", + localAsset: "/brands/apps/github.svg", + }], + }), + })); + root = createRoot(container); + + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.querySelector("img")).toBeNull(); + + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.querySelector("img")?.getAttribute("src")).toBe("/brands/apps/github.svg"); + }); + + it("keeps the caller logo when the manifest has no matching provider", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ schemaVersion: 1, providers: [] }), + })); + root = createRoot(container); + + await act(async () => { + root.render(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.querySelector("img")?.getAttribute("src")).toBe("https://remote.example/custom.svg"); + }); +}); diff --git a/ui/src/pages/apps/AppLogo.tsx b/ui/src/pages/apps/AppLogo.tsx index 3a570175736..199e264a931 100644 --- a/ui/src/pages/apps/AppLogo.tsx +++ b/ui/src/pages/apps/AppLogo.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { loadLocalAppBrandAssets, type LocalAppBrandAssets } from "@/lib/app-brand-assets"; import { cn } from "@/lib/utils"; const TILE_COLORS = [ @@ -20,7 +21,9 @@ function colorFor(seed: string): string { interface AppLogoProps { name: string; + brandKey?: string | null; logoUrl?: string | null; + allowRemoteFallback?: boolean; size?: number; className?: string; } @@ -30,25 +33,123 @@ interface AppLogoProps { * favicon when available, falling back to a coloured letter tile (deterministic * colour per app name) when the image is missing or fails to load. */ -export function AppLogo({ name, logoUrl, size = 36, className }: AppLogoProps) { - const [failed, setFailed] = useState(false); +export function AppLogo({ + name, + brandKey, + logoUrl, + allowRemoteFallback = true, + size = 36, + className, +}: AppLogoProps) { + const [failedLogoUrls, setFailedLogoUrls] = useState>(() => new Set()); + const lookupKey = brandKey?.trim() || name; + const [localAssetResult, setLocalAssetResult] = useState<{ + lookupKey: string; + assets: LocalAppBrandAssets | null; + } | null>(null); + const localLookupComplete = localAssetResult?.lookupKey === lookupKey; + const localAssets = localLookupComplete ? localAssetResult.assets : null; const letter = (name.trim()[0] ?? "?").toUpperCase(); const dimension = { width: size, height: size }; + // Do not expose a remote caller URL until the local manifest has had a + // chance to resolve this provider. Otherwise the browser requests the + // remote asset during the first render even when a bundled mark exists. + const resolvedLogoUrl = localLookupComplete + ? localAssets?.light ?? (allowRemoteFallback ? logoUrl : null) + : null; + const lightLogoUrl = resolvedLogoUrl && !failedLogoUrls.has(resolvedLogoUrl) + ? resolvedLogoUrl + : null; + const requestedDarkLogoUrl = localAssets?.dark ?? null; + const darkLogoUrl = requestedDarkLogoUrl && !failedLogoUrls.has(requestedDarkLogoUrl) + ? requestedDarkLogoUrl + : null; + const hasDistinctThemeLogos = Boolean( + resolvedLogoUrl && requestedDarkLogoUrl && resolvedLogoUrl !== requestedDarkLogoUrl, + ); + const fallbackLogoUrl = lightLogoUrl ?? darkLogoUrl; - if (logoUrl && !failed) { + useEffect(() => { + let active = true; + void loadLocalAppBrandAssets(lookupKey) + .then((assets) => { + if (active) setLocalAssetResult({ lookupKey, assets }); + }) + .catch(() => { + if (active) setLocalAssetResult({ lookupKey, assets: null }); + }); + return () => { + active = false; + }; + }, [lookupKey]); + + useEffect(() => { + setFailedLogoUrls(new Set()); + }, [resolvedLogoUrl, localAssets?.dark]); + + const markLogoFailed = (url: string) => { + setFailedLogoUrls((current) => new Set(current).add(url)); + }; + + if (fallbackLogoUrl) { return ( - setFailed(true)} - /> + {hasDistinctThemeLogos ? ( + <> + {lightLogoUrl ? ( + markLogoFailed(lightLogoUrl)} + /> + ) : ( + + )} + {darkLogoUrl ? ( + markLogoFailed(darkLogoUrl)} + /> + ) : ( + + )} + + ) : ( + markLogoFailed(fallbackLogoUrl)} + /> + )} ); } diff --git a/ui/src/pages/apps/Connections.tsx b/ui/src/pages/apps/Connections.tsx index ffa5c205237..1460e796df7 100644 --- a/ui/src/pages/apps/Connections.tsx +++ b/ui/src/pages/apps/Connections.tsx @@ -64,6 +64,7 @@ type AppRow = { application: ToolApplication; connection: ToolConnection | null; displayName: string; + brandKey: string; owner: ConnectionOwnerProfile | null; remainingAgentAvailableConnectionCount: number; status: AppStatus; @@ -230,9 +231,11 @@ export function Connections() { const rows = useMemo(() => { return applications.flatMap((application): AppRow[] => { const appConnections = connectionsByApplication.get(application.id) ?? []; - const galleryEntry = logoByKey.get(appApplicationSourceSlug(application) ?? ""); - const logoUrl = appDefinitionLogoUrl(galleryEntry) ?? - appDefinitionLogoUrl(logoByName.get(application.name.toLowerCase())); + const appSourceSlug = appApplicationSourceSlug(application); + const galleryEntry = logoByKey.get(appSourceSlug ?? "") ?? + logoByName.get(application.name.toLowerCase()); + const logoUrl = appDefinitionLogoUrl(galleryEntry); + const brandKey = appSourceSlug ?? application.name; const agentAvailableConnectionCount = appConnections.filter( (connection) => connection.status === "active" && connection.enabled, ).length; @@ -241,6 +244,7 @@ export function Connections() { application, connection: null, displayName: application.name, + brandKey, owner: null, remainingAgentAvailableConnectionCount: 0, status: statusFor(application, []), @@ -255,6 +259,7 @@ export function Connections() { application, connection, displayName: connectionDisplayNameForOwner(connection, application.name, owner), + brandKey, owner, remainingAgentAvailableConnectionCount: Math.max( 0, @@ -401,6 +406,7 @@ export function Connections() {
diff --git a/ui/src/pages/apps/app-definition-display.ts b/ui/src/pages/apps/app-definition-display.ts index 7abeb929ee4..44cf627e635 100644 --- a/ui/src/pages/apps/app-definition-display.ts +++ b/ui/src/pages/apps/app-definition-display.ts @@ -1,4 +1,4 @@ -import type { AppDefinition, ToolApplication } from "@paperclipai/shared"; +import type { AppDefinition, ToolApplication, ToolConnection } from "@paperclipai/shared"; export type AppGalleryDisplayEntry = AppDefinition & { key?: string; @@ -40,3 +40,9 @@ export function appApplicationSourceSlug(application: ToolApplication | null | u } return key; } + +export function appConnectionSourceSlug(connection: ToolConnection | null | undefined): string | null { + if (!connection) return null; + const source = connection.config?.sourceTemplateKey ?? connection.transportConfig?.sourceTemplateKey; + return typeof source === "string" && source.trim() ? source.trim() : null; +}