Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions apps/web/src/command-palette-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/<slug>` 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/<slug>` 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,
Expand Down Expand Up @@ -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);
Expand Down
59 changes: 0 additions & 59 deletions apps/web/src/pages/detail-placeholders.tsx

This file was deleted.

32 changes: 8 additions & 24 deletions apps/web/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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" ||
Expand Down Expand Up @@ -400,19 +393,10 @@ export const APP_ROUTES: readonly AppRoute[] = [
icon: <ListBullets />,
render: (path: string) => <EvalsRoute path={path} />,
},
{
path: PLUGIN_DETAIL_PATH,
label: "Plugin",
icon: <SquaresFour />,
render: (path: string) => (
<PluginDetailPlaceholder
slug={detailRouteSlug(PLUGIN_DETAIL_PATH, path)}
/>
),
},
{
// 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: <SquaresFour />,
Expand Down
36 changes: 17 additions & 19 deletions apps/web/test/routes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
APP_ROUTES,
matchesRoute,
NAV_ROUTES,
PLUGIN_DETAIL_PATH,
ROUTINE_DETAIL_PATH,
SKILL_DETAIL_PATH,
} from "../src/routes";
Expand All @@ -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`
Expand Down Expand Up @@ -160,7 +159,6 @@ describe("route table", () => {
"/settings/skills",
"/insights",
"/evals",
"/plugins/:slug",
"/plugins",
"/settings",
]);
Expand Down Expand Up @@ -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,
);
Expand All @@ -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");
Expand All @@ -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();
});
Expand All @@ -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,
Expand All @@ -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);
});

Expand Down Expand Up @@ -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/<slug> 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
Expand Down
Loading