From a833d4cf5f0bfd9056316456c8ae72029cf7fcdc Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 22:27:58 -0700 Subject: [PATCH 1/3] Add tests for Plugins on the first-run footer rail The footer rail is Routines, Files, Skills, Agents, Plugins. /plugins lights the Plugins row. Insights and Evals still join only after honest usage. --- apps/web/test/routes.test.tsx | 14 +++++++------- apps/web/test/sidebar.test.tsx | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/web/test/routes.test.tsx b/apps/web/test/routes.test.tsx index 19ae3092c..1139a3a3a 100644 --- a/apps/web/test/routes.test.tsx +++ b/apps/web/test/routes.test.tsx @@ -116,13 +116,12 @@ function stagePageTitle(markup: string): string | undefined { } /** The first-run footer rail marks its own destination current: Routines, - * Files, Skills, and Agents are text rows with `aria-current="page"` on - * the lit one. Insights and Evals join only given honest usage, and - * Plugins is never a first-run tour destination — those three stay - * reachable by URL and palette instead. Settings lives beside the - * account row, so its route lights nothing in the chrome - the stage - * title carries it. Returns the active row's label so tests confirm the - * *right* footer affordance lights, and nothing else does. */ + * Files, Skills, Agents, and Plugins are text rows with `aria-current="page"` + * on the lit one. Insights and Evals join only given honest usage, and + * stay reachable by URL and palette instead until then. Settings lives + * beside the account row, so its route lights nothing in the chrome - the + * stage title carries it. Returns the active row's label so tests confirm + * the *right* footer affordance lights, and nothing else does. */ function activeFooterLabel(markup: string): string | undefined { const lit = /shell-sidebar-footer-row"[^>]*aria-current="page"[^>]*>([\s\S]*?)<\/button>/.exec( @@ -137,6 +136,7 @@ const FOOTER_LABELS: Record = { "/files": "Files", "/skills": "Skills", "/agents": "Agents", + "/plugins": "Plugins", }; describe("route table", () => { diff --git a/apps/web/test/sidebar.test.tsx b/apps/web/test/sidebar.test.tsx index 66eefa578..413dc7af2 100644 --- a/apps/web/test/sidebar.test.tsx +++ b/apps/web/test/sidebar.test.tsx @@ -219,13 +219,14 @@ describe("Sidebar", () => { expect(markup).not.toContain("shell-rail-item"); }); - test("first-run footer rail is Routines, Files, Skills, Agents, then the account row — no Plugins, Insights, Evals, or Inbox", () => { + test("first-run footer rail is Routines, Files, Skills, Agents, Plugins, then the account row — no Insights, Evals, or Inbox", () => { const markup = renderSidebar("/w"); expect(footerRowLabelsFromMarkup(markup)).toEqual([ "Routines", "Files", "Skills", "Agents", + "Plugins", ]); expect(markup).toContain("data-ctx-account"); expect(markup).not.toContain(">Inbox<"); @@ -250,23 +251,26 @@ describe("Sidebar", () => { "Files", "Skills", "Agents", + "Plugins", ]); act(() => root.unmount()); container.remove(); }); - test("Plugins is not presented as a first-run tour destination", () => { + test("marks the Plugins row current on /plugins", () => { const onPlugins = renderSidebar("/plugins"); expect(footerRowLabelsFromMarkup(onPlugins)).toEqual([ "Routines", "Files", "Skills", "Agents", + "Plugins", ]); - expect(onPlugins).not.toContain(">Plugins<"); - expect(onPlugins).not.toMatch( - /shell-sidebar-footer-row"[^>]*aria-current="page"/, + expect(onPlugins).toMatch( + /shell-sidebar-footer-row"[^>]*data-active="true"[^>]*>[\s\S]*?>PluginsPlugins<[\s\S]{0,80}aria-current="page"/); }); test("Evals, Insights, and Plugins remain reachable by URL and command palette", () => { @@ -297,6 +301,7 @@ describe("Sidebar", () => { "Files", "Skills", "Agents", + "Plugins", "Insights", ]); const insights = [ @@ -315,6 +320,7 @@ describe("Sidebar", () => { "Files", "Skills", "Agents", + "Plugins", "Evals", ]); const evals = [ @@ -333,6 +339,7 @@ describe("Sidebar", () => { "Files", "Skills", "Agents", + "Plugins", ]); act(() => root.unmount()); container.remove(); From 89323b9e3826ccd32b30d9b6e77fbcc8cfc9c7f6 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 22:28:01 -0700 Subject: [PATCH 2/3] Put Plugins back on the first-run footer rail Plugins is a first-run destination again, after Agents, with the PuzzlePiece mark. Insights and Evals still wait on honest usage. --- apps/web/src/routes.tsx | 20 ++++++++--------- apps/web/src/shell/sidebar.tsx | 41 +++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/apps/web/src/routes.tsx b/apps/web/src/routes.tsx index eecf74abd..f15d43514 100644 --- a/apps/web/src/routes.tsx +++ b/apps/web/src/routes.tsx @@ -1,10 +1,10 @@ // The route table: one entry per screen, consumed by the command palette // (label) and the route switch (render), so navigation and pages cannot // drift apart. The sidebar itself lists workbenches (conversations), not -// routes — the first-run footer reaches Routines, Files, Skills, and -// Agents; Insights and Evals join that rail only given honest usage. -// Plugins, Insights, Evals, and Settings stay reachable by deep link and -// the palette even when they are off the rail. Conversation deep links +// routes — the first-run footer reaches Routines, Files, Skills, Agents, +// and Plugins; Insights and Evals join that rail only given honest usage. +// Insights, Evals, and Settings stay reachable by deep link and the +// palette even when they are off the rail. Conversation deep links // (`/w/:workbenchId`) stay routable; `/` is the Myra land hop (ensure + // open her conversation) for a bench with a workbench already, or the // guided first-workbench describe screen for a bench with none (CL-6104) @@ -26,6 +26,7 @@ import { FolderOpen, Lightning, ListBullets, + PuzzlePiece, Robot, SlidersHorizontal, SquaresFour, @@ -395,12 +396,11 @@ export const APP_ROUTES: readonly AppRoute[] = [ render: (path: string) => , }, { - // Reached by deep link and the command palette's Pages group — never - // from the first-run footer rail. No `/plugins/:slug` until CL-6417 - // (CL-6817 unlinked the stub). + // First-run footer rail destination. No `/plugins/:slug` until + // CL-6417 (CL-6817 unlinked the stub). path: "/plugins", label: "Plugins", - icon: , + icon: , render: (path: string, navigate: (to: string) => void) => ( ), @@ -426,8 +426,8 @@ function routesInOrder(paths: readonly string[]): readonly AppRoute[] { /** * Everything the command palette treats as a product destination (its * "Pages" group). The first-run sidebar footer reaches Routines / Files / - * Skills / Agents (and Insights / Evals only given honest usage); - * Plugins, Insights, Evals, and Settings stay palette- and + * Skills / Agents / Plugins (and Insights / Evals only given honest + * usage); Insights, Evals, and Settings stay palette- and * deep-link-reachable even when they are off the rail. */ export const NAV_ROUTES: readonly AppRoute[] = routesInOrder([ diff --git a/apps/web/src/shell/sidebar.tsx b/apps/web/src/shell/sidebar.tsx index 4bb80b751..fc6d9170b 100644 --- a/apps/web/src/shell/sidebar.tsx +++ b/apps/web/src/shell/sidebar.tsx @@ -1,14 +1,12 @@ // The one sidebar. Header: the brand mark, then create + search. Body: // Agents and Channels — nothing page-scoped ever renders here. Footer: the -// first-run rail is Routines, Files, Skills, Agents; Insights and Evals -// join only when the existing usage / eval-run reads return real items -// (never a fabricated row, never a new analytics store). Plugins is -// reachable by URL and the command palette, not as a first-run tour -// destination. Below the rail: the account row — avatar + name, the whole -// row is the trigger for a menu that pops upward with weekly usage, -// settings, feedback, and log out. Always present; there is no collapse -// affordance and no second nav column. Approvals belong in the -// conversation, not as a standing band here. +// first-run rail is Routines, Files, Skills, Agents, Plugins; Insights and +// Evals join only when the existing usage / eval-run reads return real items +// (never a fabricated row, never a new analytics store). Below the rail: +// the account row — avatar + name, the whole row is the trigger for a menu +// that pops upward with weekly usage, settings, feedback, and log out. +// Always present; there is no collapse affordance and no second nav column. +// Approvals belong in the conversation, not as a standing band here. // // Inbox is gone (CL-6151, owner decision: tasks + approvals don't flow // into workbenches). @@ -41,6 +39,7 @@ import { Lightning, ListBullets, Plus, + PuzzlePiece, Robot, SignOut, Repeat, @@ -170,7 +169,7 @@ export function Sidebar({ {/* Mission Control is pinned above the footer rail as its own row (DESIGN.md's Shell & Navigation) — not a button inside the - first-run rail, which stays Routines/Files/Skills/Agents. */} + first-run rail, which stays Routines/Files/Skills/Agents/Plugins. */}
+ {showInsights ? (