From 110a4d76f8cb39feba9d4e14791cfcece33379d2 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 23 Aug 2026 11:02:24 -0700 Subject: [PATCH 1/2] Add tests for unlinking the plugin detail stub (CL-6817) Pin that /plugins/ is not-found with no "still being built" placeholder until CL-6417 lands a real detail page. --- apps/web/test/routes.test.tsx | 36 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/apps/web/test/routes.test.tsx b/apps/web/test/routes.test.tsx index f37332c1d..2a30bcd42 100644 --- a/apps/web/test/routes.test.tsx +++ b/apps/web/test/routes.test.tsx @@ -17,7 +17,6 @@ import { APP_ROUTES, matchesRoute, NAV_ROUTES, - PLUGIN_DETAIL_PATH, ROUTINE_DETAIL_PATH, SKILL_DETAIL_PATH, } from "../src/routes"; @@ -26,12 +25,12 @@ import type { SessionState } from "../src/session"; /** Slug-addressed detail routes (CL-6412). The generic render loop below * renders each `route.path` verbatim, which for these is the pattern * (`/agents/:slug`) rather than a real path - they get their own render - * tests instead. */ + * tests instead. Plugin detail (`/plugins/:slug`) is intentionally absent + * until CL-6417 — the stub was unlinked in CL-6817. */ const DETAIL_ROUTE_PATHS = new Set([ ROUTINE_DETAIL_PATH, AGENT_DETAIL_PATH, SKILL_DETAIL_PATH, - PLUGIN_DETAIL_PATH, ]); /** Legacy routes that only redirect - `/library` bounces to `/files` @@ -160,7 +159,6 @@ describe("route table", () => { "/settings/skills", "/insights", "/evals", - "/plugins/:slug", "/plugins", "/settings", ]); @@ -221,7 +219,6 @@ describe("route table", () => { test("a slug segment resolves to the entity's own detail route (CL-6412)", () => { expect(matchesRoute(AGENT_DETAIL_PATH, "/agents/triage-bot")).toBe(true); expect(matchesRoute(SKILL_DETAIL_PATH, "/skills/pr-review")).toBe(true); - expect(matchesRoute(PLUGIN_DETAIL_PATH, "/plugins/linear")).toBe(true); expect(matchesRoute(ROUTINE_DETAIL_PATH, "/routines/weekly-digest")).toBe( true, ); @@ -242,7 +239,6 @@ describe("route table", () => { APP_ROUTES.find((candidate) => matchesRoute(candidate.path, path))?.path; expect(routeFor("/agents/triage-bot")).toBe(AGENT_DETAIL_PATH); expect(routeFor("/skills/pr-review")).toBe(SKILL_DETAIL_PATH); - expect(routeFor("/plugins/linear")).toBe(PLUGIN_DETAIL_PATH); expect(routeFor("/routines/weekly-digest")).toBe(ROUTINE_DETAIL_PATH); expect(routeFor("/agents/wfd_1")).toBe("/agents"); expect(routeFor("/skills/skill_1")).toBe("/skills"); @@ -251,11 +247,13 @@ describe("route table", () => { expect(routeFor("/routines/rtn_1")).toBe(ROUTINE_DETAIL_PATH); }); - test("the Plugins roster owns its bare path and slug details, nothing else", () => { + test("the Plugins roster owns only its bare path until CL-6417 lands a detail page (CL-6817)", () => { const routeFor = (path: string) => APP_ROUTES.find((candidate) => matchesRoute(candidate.path, path))?.path; expect(routeFor("/plugins")).toBe("/plugins"); - expect(routeFor("/plugins/linear")).toBe(PLUGIN_DETAIL_PATH); + // No stub detail: a slug under /plugins is unroutable, not a + // "still being built" placeholder (CL-6817). + expect(routeFor("/plugins/linear")).toBeUndefined(); expect(routeFor("/plugins/Linear")).toBeUndefined(); expect(routeFor("/plugins/linear/settings")).toBeUndefined(); }); @@ -267,7 +265,7 @@ describe("route table", () => { // A segment that cannot be decoded names no routine, so the detail // route declines it and the roster answers instead. expect(routeFor("/routines/%E0%A4%A")).toBe("/routines"); - expect(matchesRoute(PLUGIN_DETAIL_PATH, "/plugins/%")).toBe(false); + expect(matchesRoute("/plugins", "/plugins/%")).toBe(false); expect(matchesRoute(ROUTINE_DETAIL_PATH, "/routines/%E0%A4%A")).toBe(false); expect(matchesRoute(AGENT_DETAIL_PATH, "/agents/%2Ftriage-bot")).toBe( false, @@ -277,7 +275,7 @@ describe("route table", () => { test("a detail path keeps its roster's sidebar row lit", () => { expect(matchesRoute("/agents", "/agents/triage-bot")).toBe(true); expect(matchesRoute("/skills", "/skills/pr-review")).toBe(true); - expect(matchesRoute("/plugins", "/plugins/linear")).toBe(true); + expect(matchesRoute("/plugins", "/plugins/linear")).toBe(false); expect(matchesRoute("/routines", "/routines/weekly-digest")).toBe(true); }); @@ -368,15 +366,15 @@ describe("routes render", () => { expect(activeFooterLabel(markup)).toBe("Skills"); }); - test.each([["/plugins/linear", "linear", "Plugins"]])( - "%s titles the detail placeholder %s without lighting a Plugins footer row", - async (path, slug, rosterLabel) => { - const markup = await renderApp(path); - expect(stagePageTitle(markup)).toBe(slug); - expect(markup).toContain(`Back to ${rosterLabel}`); - expect(activeFooterLabel(markup)).toBeUndefined(); - }, - ); + // CL-6817: the plugin detail stub ("still being built") is gone until + // CL-6417 ships a real page. A slug under /plugins must not promise one. + test("/plugins/ is not-found, never a still-being-built stub (CL-6817)", async () => { + const markup = await renderApp("/plugins/linear"); + expect(markup).toContain("Page not found"); + expect(markup).not.toContain("still being built"); + expect(markup).not.toContain("Back to Plugins"); + expect(activeFooterLabel(markup)).toBeUndefined(); + }); test("a routine segment that resolves to nothing still titles itself and lights Routines", async () => { // Routines is a real page now, not a placeholder: with no routine From a8883815ae955d1521d48709e3d9c0fcc476eb97 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 23 Aug 2026 11:02:27 -0700 Subject: [PATCH 2/2] Unlink plugin detail stub until CL-6417 (CL-6817) Remove the /plugins/:slug "still being built" placeholder and send command-palette plugin picks to the gallery instead of promising a page that is only a stub. --- apps/web/src/command-palette-provider.tsx | 12 ++--- apps/web/src/pages/detail-placeholders.tsx | 59 ---------------------- apps/web/src/routes.tsx | 32 +++--------- 3 files changed, 14 insertions(+), 89 deletions(-) delete mode 100644 apps/web/src/pages/detail-placeholders.tsx diff --git a/apps/web/src/command-palette-provider.tsx b/apps/web/src/command-palette-provider.tsx index d834945e0..0fcd89bf4 100644 --- a/apps/web/src/command-palette-provider.tsx +++ b/apps/web/src/command-palette-provider.tsx @@ -274,11 +274,9 @@ export function CommandPaletteProvider({ open && selectedTenantId !== null, () => listSkills(selectedTenantId ?? ""), ); - // Plugins, as far as this bench has any: its connected MCP servers, each - // already carrying the immutable slug `/plugins/` is addressed by. - // The gallery's presets and catalog entries are not connected things and - // have no detail route of their own yet — a follow-up, not a second - // search. + // Connected MCP servers for this bench. Until CL-6417 lands a real + // `/plugins/` page, selecting one opens the Plugins gallery + // (CL-6817) rather than a "still being built" stub. const mcpServersQuery = useTenantQuery( tenantKeys.mcpServers(selectedTenantId ?? ""), open && selectedTenantId !== null, @@ -578,7 +576,9 @@ export function CommandPaletteProvider({ } else if (id.startsWith("entity:plugins:")) { const slug = id.slice("entity:plugins:".length); const title = pluginItems.find((item) => item.id === id)?.title ?? slug; - navigate(detailPath(PLUGINS_PATH_PREFIX, { slug, id: slug })); + // No plugin detail page yet (CL-6417 parked). Land on the gallery + // instead of the removed stub (CL-6817). + navigate(PLUGINS_PATH_PREFIX); pushRecent({ kind: "plugins", id, title, subtitle: "Plugin" }); } else if (id.startsWith("entity:library:")) { const artifactId = id.slice("entity:library:".length); diff --git a/apps/web/src/pages/detail-placeholders.tsx b/apps/web/src/pages/detail-placeholders.tsx deleted file mode 100644 index 13d97d48f..000000000 --- a/apps/web/src/pages/detail-placeholders.tsx +++ /dev/null @@ -1,59 +0,0 @@ -// Placeholders for the slug-addressed detail screens (CL-6412 routes them; -// the screens themselves are their own tickets). Each renders the slug it -// was routed for and a way back to its roster, so the route is real and -// testable before the page behind it exists. - -import { Button, EmptyState, PageShell } from "@corbits/react-ui"; -import { SquaresFour } from "@corbits/icons"; -import type { Slug } from "@corbits/slug"; -import type { ReactNode } from "react"; - -import { Link } from "../navigation"; -import { PLUGINS_PATH_PREFIX } from "../path-ids"; -import { StageTopBar } from "../shell/stage-top-bar"; - -function DetailPlaceholder({ - slug, - entity, - rosterLabel, - rosterPath, - icon, -}: { - readonly slug: Slug; - readonly entity: string; - readonly rosterLabel: string; - readonly rosterPath: string; - readonly icon: ReactNode; -}) { - return ( -
- - - - {`Back to ${rosterLabel}`} - - } - /> - -
- ); -} - -export function PluginDetailPlaceholder({ slug }: { readonly slug: Slug }) { - return ( - } - /> - ); -} diff --git a/apps/web/src/routes.tsx b/apps/web/src/routes.tsx index 91a317001..37678acd0 100644 --- a/apps/web/src/routes.tsx +++ b/apps/web/src/routes.tsx @@ -36,7 +36,6 @@ import { lazy, useEffect, type ReactElement, type ReactNode } from "react"; import { AGENTS_PATH_PREFIX, EVALS_PATH_PREFIX, - PLUGINS_PATH_PREFIX, SKILLS_PATH_PREFIX, ROUTINES_PATH_PREFIX, detailSlugFromPath, @@ -96,10 +95,6 @@ const AgentDetailRoute = lazy(async () => ({ const SkillDetailRoute = lazy(async () => ({ default: (await import("./pages/skill-detail-page")).SkillDetailRoute, })); -const PluginDetailPlaceholder = lazy(async () => ({ - default: (await import("./pages/detail-placeholders")) - .PluginDetailPlaceholder, -})); const RoutineDetailRoute = lazy(async () => ({ default: (await import("./pages/routine-detail-page")).RoutineDetailRoute, })); @@ -143,7 +138,9 @@ const SLUG_SEGMENT = "/:slug"; export const AGENT_DETAIL_PATH = `${AGENTS_PATH_PREFIX}${SLUG_SEGMENT}`; export const SKILL_DETAIL_PATH = `${SKILLS_PATH_PREFIX}${SLUG_SEGMENT}`; -export const PLUGIN_DETAIL_PATH = `${PLUGINS_PATH_PREFIX}${SLUG_SEGMENT}`; +// Plugin detail (`/plugins/:slug`) is parked with CL-6417. CL-6817 removed +// the "still being built" stub so gallery/palette click-throughs do not +// promise a page that is only a placeholder. /** * Routines are addressed by id, not by slug. DESIGN.md allows a slug in a @@ -208,10 +205,9 @@ export type AppRoute = { * conversation deep links (which also match when Myra land `/` is active) * and the slug-addressed detail routes (`/agents/:slug`). Other routes are * exact path matches. A roster prefix still matches its own nested paths, - * so the sidebar footer row stays lit on a detail screen — except Plugins, - * whose roster consumes no path segment of its own: there, only the bare - * path and a slug detail resolve, and any other nested path is unroutable - * rather than quietly showing the roster. + * so the sidebar footer row stays lit on a detail screen. Plugins is exact + * only: until CL-6417 lands a real detail page, a slug under `/plugins` is + * unroutable rather than a stub (CL-6817). */ export function matchesRoute(routePath: string, path: string): boolean { if (routePath === WORKBENCH_PATH_PREFIX) { @@ -224,9 +220,6 @@ export function matchesRoute(routePath: string, path: string): boolean { if (routePath.endsWith(SLUG_SEGMENT)) { return slugForDetailRoute(routePath, path) !== null; } - if (routePath === PLUGINS_PATH_PREFIX) { - return path === routePath || detailSlugFromPath(path, routePath) !== null; - } if ( routePath === "/routines" || routePath === "/library" || @@ -400,19 +393,10 @@ export const APP_ROUTES: readonly AppRoute[] = [ icon: , render: (path: string) => , }, - { - path: PLUGIN_DETAIL_PATH, - label: "Plugin", - icon: , - render: (path: string) => ( - - ), - }, { // Reached by deep link and the command palette's Pages group — never - // from the first-run footer rail. + // from the first-run footer rail. No `/plugins/:slug` until CL-6417 + // (CL-6817 unlinked the stub). path: "/plugins", label: "Plugins", icon: ,