From 438c2c085c4d34de70107fd4fd5dce2180c2bbe0 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 26 May 2026 12:32:25 +0000 Subject: [PATCH] Badge experimental bundled plugins --- server/src/__tests__/plugin-routes-authz.test.ts | 7 +++++++ server/src/routes/plugins.ts | 14 ++++++++++++++ ui/src/api/plugins.ts | 1 + ui/src/pages/PluginManager.tsx | 13 +++++++++++++ 4 files changed, 35 insertions(+) diff --git a/server/src/__tests__/plugin-routes-authz.test.ts b/server/src/__tests__/plugin-routes-authz.test.ts index 9447bc93928..90371991576 100644 --- a/server/src/__tests__/plugin-routes-authz.test.ts +++ b/server/src/__tests__/plugin-routes-authz.test.ts @@ -146,11 +146,18 @@ describe.sequential("plugin install and upgrade authz", () => { expect(res.status).toBe(200); const packageNames = res.body.map((plugin: { packageName: string }) => plugin.packageName); + const byPackageName = new Map( + res.body.map((plugin: { packageName: string; experimental: boolean }) => [plugin.packageName, plugin]), + ); expect(packageNames).toContain("@paperclipai/plugin-workspace-diff"); expect(packageNames).toContain("@paperclipai/plugin-llm-wiki"); expect(packageNames).toContain("@paperclipai/plugin-modal"); expect(packageNames).toContain("@paperclipai/plugin-authoring-smoke-example"); expect(packageNames).not.toContain("@paperclipai/plugin-sdk"); + expect(byPackageName.get("@paperclipai/plugin-workspace-diff")?.experimental).toBe(true); + expect(byPackageName.get("@paperclipai/plugin-llm-wiki")?.experimental).toBe(true); + expect(byPackageName.get("@paperclipai/plugin-modal")?.experimental).toBe(true); + expect(byPackageName.get("@paperclipai/plugin-authoring-smoke-example")?.experimental).toBe(false); }, 20_000); it("rejects plugin installation for non-admin board users", async () => { diff --git a/server/src/routes/plugins.ts b/server/src/routes/plugins.ts index 9f48a0dc5f5..16434ef9011 100644 --- a/server/src/routes/plugins.ts +++ b/server/src/routes/plugins.ts @@ -121,6 +121,7 @@ interface AvailableBundledPlugin { description: string; localPath: string; tag: "example" | "first-party"; + experimental: boolean; } /** Response body for GET /api/plugins/:pluginId/health */ @@ -150,6 +151,10 @@ const PLUGIN_SCOPED_API_RESPONSE_HEADER_ALLOWLIST = new Set([ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, "../../.."); +const EXPERIMENTAL_BUNDLED_PLUGIN_PACKAGE_NAMES = new Set([ + "@paperclipai/plugin-llm-wiki", + "@paperclipai/plugin-workspace-diff", +]); function titleCasePluginName(packageName: string): string { const localName = packageName.split("/").pop() ?? packageName; @@ -244,6 +249,14 @@ function bundledPluginMetadata( } } +function isExperimentalBundledPlugin(packageRoot: string, packageName: string): boolean { + return ( + EXPERIMENTAL_BUNDLED_PLUGIN_PACKAGE_NAMES.has(packageName) + || packageRoot.includes(`${path.sep}sandbox-providers${path.sep}`) + || packageName.includes("sandbox") + ); +} + function listBundledPlugins(): AvailableBundledPlugin[] { const pluginRoot = path.resolve(REPO_ROOT, "packages/plugins"); return findPackageJsonFiles(pluginRoot) @@ -274,6 +287,7 @@ function listBundledPlugins(): AvailableBundledPlugin[] { ?? `Bundled Paperclip plugin from ${path.relative(REPO_ROOT, packageRoot)}.`, localPath: packageRoot, tag, + experimental: isExperimentalBundledPlugin(packageRoot, packageName), }, ]; }) diff --git a/ui/src/api/plugins.ts b/ui/src/api/plugins.ts index 8f4a548612d..b018756091e 100644 --- a/ui/src/api/plugins.ts +++ b/ui/src/api/plugins.ts @@ -139,6 +139,7 @@ export interface AvailableBundledPlugin { description: string; localPath: string; tag: "example" | "first-party"; + experimental: boolean; } export interface PluginLocalFolderProblem { diff --git a/ui/src/pages/PluginManager.tsx b/ui/src/pages/PluginManager.tsx index d03c1f95771..7ae4f2dc301 100644 --- a/ui/src/pages/PluginManager.tsx +++ b/ui/src/pages/PluginManager.tsx @@ -43,6 +43,17 @@ function getPluginErrorSummary(plugin: PluginRecord): string { return firstNonEmptyLine(plugin.lastError) ?? "Plugin entered an error state without a stored error message."; } +function ExperimentalBadge() { + return ( + + Experimental + + ); +} + /** * PluginManager page component. * @@ -249,6 +260,7 @@ export function PluginManager() { {bundledPlugin.tag === "first-party" ? "First-party" : "Example"} + {bundledPlugin.experimental && } {installedPlugin ? ( )} + {bundledByPackageName.get(plugin.packageName)?.experimental && }