diff --git a/apps/app/src/hooks/queries/plugin-settings-queries.ts b/apps/app/src/hooks/queries/plugin-settings-queries.ts
index 2456b750a37..7e9d7bbc6a5 100644
--- a/apps/app/src/hooks/queries/plugin-settings-queries.ts
+++ b/apps/app/src/hooks/queries/plugin-settings-queries.ts
@@ -52,6 +52,7 @@ export interface PluginListItem {
source: string;
isOrphanedBuiltin: boolean;
catalogEntryId: string | null;
+ catalogMarketplaceName: string | null;
publisherLabel: string | null;
sourceDisplay: string;
updateState: PluginUpdateState;
@@ -108,6 +109,7 @@ export function toPluginListItem(plugin: InstalledPlugin): PluginListItem {
source: plugin.source,
isOrphanedBuiltin: plugin.isOrphanedBuiltin,
catalogEntryId: plugin.catalogEntryId ?? null,
+ catalogMarketplaceName: plugin.catalogMarketplaceName ?? null,
publisherLabel: plugin.publisherLabel,
sourceDisplay: plugin.sourceDisplay,
updateState: {
diff --git a/apps/app/src/test/fixtures/plugins.ts b/apps/app/src/test/fixtures/plugins.ts
index eee2d172319..9e2053505cd 100644
--- a/apps/app/src/test/fixtures/plugins.ts
+++ b/apps/app/src/test/fixtures/plugins.ts
@@ -67,6 +67,7 @@ export function makePluginListItem(
provenance: "direct",
isOrphanedBuiltin: false,
catalogEntryId: null,
+ catalogMarketplaceName: null,
publisherLabel: null,
sourceDisplay: "path ยท /plugins/plugin-test",
updateState: {
diff --git a/apps/app/src/views/ToolsView.plugin-detail.test.tsx b/apps/app/src/views/ToolsView.plugin-detail.test.tsx
index 5059c307880..a3fac8194a0 100644
--- a/apps/app/src/views/ToolsView.plugin-detail.test.tsx
+++ b/apps/app/src/views/ToolsView.plugin-detail.test.tsx
@@ -589,6 +589,168 @@ describe("PluginDetail official catalog lifecycle", () => {
});
describe("BB Official plugin detail routing", () => {
+ it("uses the installed catalog identity when plugin ids collide", async () => {
+ const firstCatalogEntry = {
+ ...GITHUB_CATALOG_ENTRY,
+ marketplace: "bb-community",
+ marketplaceDisplayName: "BB Community",
+ publisherKey: "bb-community:example",
+ publisherLabel: "BB Community",
+ description: "Description from the first catalog.",
+ repositoryUrl: "https://github.com/example/first-catalog-plugin",
+ author: {
+ name: "First publisher",
+ github: "first-publisher",
+ url: "https://github.com/first-publisher",
+ },
+ };
+ const installedCatalogEntry = {
+ ...GITHUB_CATALOG_ENTRY,
+ marketplace: "partner-catalog",
+ marketplaceDisplayName: "Partner Catalog",
+ publisherKey: "partner-catalog:example",
+ publisherLabel: "Partner Catalog",
+ description: "Description from the installed catalog.",
+ repositoryUrl: "https://github.com/example/installed-catalog-plugin",
+ author: {
+ name: "Installed publisher",
+ github: "installed-publisher",
+ url: "https://github.com/installed-publisher",
+ },
+ installed: true,
+ };
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url === "/api/v1/plugins") {
+ return new Response(
+ JSON.stringify({
+ enabled: true,
+ plugins: [
+ {
+ ...GITHUB_PLUGIN,
+ catalogMarketplaceName: "partner-catalog",
+ publisherLabel: "Partner Catalog",
+ iconUrl: null,
+ screenshots: [],
+ collections: [],
+ providerIds: [],
+ icons: {},
+ updateState: {},
+ },
+ ],
+ }),
+ { headers: { "content-type": "application/json" } },
+ );
+ }
+ if (url.startsWith("/api/v1/plugin-catalog/search")) {
+ return new Response(
+ JSON.stringify({
+ results: [firstCatalogEntry, installedCatalogEntry],
+ collections: [],
+ }),
+ { headers: { "content-type": "application/json" } },
+ );
+ }
+ return new Response(JSON.stringify({ error: "not found" }), {
+ status: 404,
+ headers: { "content-type": "application/json" },
+ });
+ }),
+ );
+
+ const { wrapper: QueryClientWrapper } = createQueryClientTestHarness();
+ render(
+
+
+ } />
+
+ ,
+ { wrapper: QueryClientWrapper },
+ );
+
+ await waitFor(() => {
+ expect(
+ document.querySelector("[data-plugin-summary]")?.textContent,
+ ).toBe("Description from the installed catalog.");
+ });
+ });
+
+ it("uses installed metadata when its catalog entry is unavailable", async () => {
+ const unrelatedCatalogEntry = {
+ ...GITHUB_CATALOG_ENTRY,
+ marketplace: "bb-community",
+ marketplaceDisplayName: "BB Community",
+ publisherKey: "bb-community:example",
+ publisherLabel: "BB Community",
+ description: "Description from an unrelated catalog entry.",
+ repositoryUrl: "https://github.com/example/unrelated-catalog-plugin",
+ author: {
+ name: "Unrelated publisher",
+ github: "unrelated-publisher",
+ url: "https://github.com/unrelated-publisher",
+ },
+ };
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url === "/api/v1/plugins") {
+ return new Response(
+ JSON.stringify({
+ enabled: true,
+ plugins: [
+ {
+ ...GITHUB_PLUGIN,
+ description: "Description from installed metadata.",
+ catalogMarketplaceName: "partner-catalog",
+ publisherLabel: "Partner Catalog",
+ iconUrl: null,
+ screenshots: [],
+ collections: [],
+ providerIds: [],
+ icons: {},
+ updateState: {},
+ },
+ ],
+ }),
+ { headers: { "content-type": "application/json" } },
+ );
+ }
+ if (url.startsWith("/api/v1/plugin-catalog/search")) {
+ return new Response(
+ JSON.stringify({
+ results: [unrelatedCatalogEntry],
+ collections: [],
+ }),
+ { headers: { "content-type": "application/json" } },
+ );
+ }
+ return new Response(JSON.stringify({ error: "not found" }), {
+ status: 404,
+ headers: { "content-type": "application/json" },
+ });
+ }),
+ );
+
+ const { wrapper: QueryClientWrapper } = createQueryClientTestHarness();
+ render(
+
+
+ } />
+
+ ,
+ { wrapper: QueryClientWrapper },
+ );
+
+ await waitFor(() => {
+ expect(
+ document.querySelector("[data-plugin-summary]")?.textContent,
+ ).toBe("Description from installed metadata.");
+ });
+ });
+
it("resolves an uninstalled catalog plugin and opens its install confirmation", async () => {
vi.stubGlobal(
"fetch",
@@ -763,6 +925,7 @@ describe("BB Official plugin detail routing", () => {
plugins: [
{
...GITHUB_PLUGIN,
+ catalogMarketplaceName: "bb-official",
iconUrl: null,
screenshots: [],
collections: [],
diff --git a/apps/app/src/views/ToolsView.tsx b/apps/app/src/views/ToolsView.tsx
index 765f45c48bc..056e5dc4d31 100644
--- a/apps/app/src/views/ToolsView.tsx
+++ b/apps/app/src/views/ToolsView.tsx
@@ -224,9 +224,23 @@ function PluginDetailToolView({ pluginId }: { pluginId: string }) {
const isLoading = listQuery.isFetching && listQuery.data === undefined;
const selectedPlugin =
plugins.find((plugin) => plugin.id === pluginId) ?? null;
+ const selectedCatalogEntryId = selectedPlugin?.catalogEntryId ?? null;
+ const selectedCatalogMarketplaceName =
+ selectedPlugin?.catalogMarketplaceName ?? null;
const selectedCatalogEntry =
- catalogQuery.data?.entries.find((entry) => entry.pluginId === pluginId) ??
- null;
+ selectedPlugin === null
+ ? (catalogQuery.data?.entries.find(
+ (entry) => entry.pluginId === pluginId,
+ ) ?? null)
+ : selectedCatalogEntryId === null ||
+ selectedCatalogMarketplaceName === null
+ ? null
+ : (catalogQuery.data?.entries.find(
+ (entry) =>
+ entry.pluginId === selectedPlugin.id &&
+ entry.entryId === selectedCatalogEntryId &&
+ entry.marketplace === selectedCatalogMarketplaceName,
+ ) ?? null);
useResourceRouteLabel(
selectedPlugin?.name ??
selectedPlugin?.id ??