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
14 changes: 6 additions & 8 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ Top to bottom:
Nothing page-scoped ever renders in this body; it lists
conversations, not product sections.
3. **Footer rail** — Mission Control is pinned above the rail as its own
row. The first-run rail below it is Routines, Files, Skills, Agents, in
that order (CL-6765). Insights and Evals join that rail only when
existing reads prove real usage (turns > 0 / at least one eval run);
Plugins stays off the rail entirely and is reached by deep link or the
command palette. These are utility destinations, not workbenches, and
each is its own top-level route (`/mission-control`, `/routines`,
`/files`, `/skills`, `/agents`; plus `/insights`, `/evals`, `/plugins`
when reached).
row. The first-run rail below it is Routines, Files, Skills, Agents,
Plugins, in that order. Insights and Evals join that rail only when
existing reads prove real usage (turns > 0 / at least one eval run).
These are utility destinations, not workbenches, and each is its own
top-level route (`/mission-control`, `/routines`, `/files`, `/skills`,
`/agents`, `/plugins`; plus `/insights`, `/evals` when reached).
4. **Account row** — avatar and name, anchoring the rail, plus a separate
settings icon beside it. The avatar+name half is a menu trigger
(weekly usage, feedback, log out) that pops upward; the gear is a
Expand Down
11 changes: 5 additions & 6 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,12 @@ A bench that already has one or more workbenches skips first-run and
lands on `workbenches[0]` (see `apps/web/src/pages/home-page.tsx`). Myra
is the first-run guide in her DM, not a home slot.

The shell's first-run destinations stay small on purpose (CL-6765):
The shell's first-run destinations stay small on purpose:
Mission Control is pinned above the footer rail; the rail itself is
Routines, Files, Skills, and Agents. Insights and Evals appear on that
rail only after honest usage exists; Plugins stays reachable by deep
link and the command palette, not as a first-run rail item. New benches
should not meet an empty Plugins / Insights / Evals gallery before they
have anything to put there.
Routines, Files, Skills, Agents, and Plugins. Insights and Evals appear
on that rail only after honest usage exists. New benches should not
meet an empty Insights / Evals gallery before they have anything to
put there.

### Code review's first minute

Expand Down
20 changes: 10 additions & 10 deletions apps/web/src/routes.tsx
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -26,6 +26,7 @@ import {
FolderOpen,
Lightning,
ListBullets,
PuzzlePiece,
Robot,
SlidersHorizontal,
SquaresFour,
Expand Down Expand Up @@ -395,12 +396,11 @@ export const APP_ROUTES: readonly AppRoute[] = [
render: (path: string) => <EvalsRoute path={path} />,
},
{
// 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: <SquaresFour />,
icon: <PuzzlePiece />,
render: (path: string, navigate: (to: string) => void) => (
<PluginsRoute path={path} navigate={navigate} />
),
Expand All @@ -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([
Expand Down
41 changes: 25 additions & 16 deletions apps/web/src/shell/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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).
Expand Down Expand Up @@ -41,6 +39,7 @@ import {
Lightning,
ListBullets,
Plus,
PuzzlePiece,
Robot,
SignOut,
Repeat,
Expand Down Expand Up @@ -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. */}
<div className="shell-sidebar-mission-control">
<button
type="button"
Expand All @@ -189,12 +188,12 @@ export function Sidebar({
</div>

<SidebarPanelFooter>
{/* Footer order: Routines, Files, Skills, Agents, then Insights and
Evals only when those existing reads prove real items, then the
account row anchors everything else (weekly usage, Settings,
Log out) in its pop-up menu — a single footer, never two stacked
rows. Routines (CL-6362) is global-only here — no per-workbench
routines chrome remains. */}
{/* Footer order: Routines, Files, Skills, Agents, Plugins, then
Insights and Evals only when those existing reads prove real
items, then the account row anchors everything else (weekly
usage, Settings, Log out) in its pop-up menu — a single footer,
never two stacked rows. Routines (CL-6362) is global-only here
— no per-workbench routines chrome remains. */}
<button
type="button"
className="shell-sidebar-footer-row"
Expand Down Expand Up @@ -235,6 +234,16 @@ export function Sidebar({
<Robot />
<span>Agents</span>
</button>
<button
type="button"
className="shell-sidebar-footer-row"
data-active={matchesRoute("/plugins", path) ? "true" : undefined}
aria-current={matchesRoute("/plugins", path) ? "page" : undefined}
onClick={() => onNavigate("/plugins")}
>
<PuzzlePiece />
<span>Plugins</span>
</button>
{showInsights ? (
<button
type="button"
Expand Down
14 changes: 7 additions & 7 deletions apps/web/test/routes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -137,6 +136,7 @@ const FOOTER_LABELS: Record<string, string> = {
"/files": "Files",
"/skills": "Skills",
"/agents": "Agents",
"/plugins": "Plugins",
};

describe("route table", () => {
Expand Down
17 changes: 12 additions & 5 deletions apps/web/test/sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<");
Expand All @@ -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]*?>Plugins</,
);
const elsewhere = renderSidebar("/w");
expect(elsewhere).not.toMatch(/>Plugins<[\s\S]{0,80}aria-current="page"/);
});

test("Evals, Insights, and Plugins remain reachable by URL and command palette", () => {
Expand Down Expand Up @@ -297,6 +301,7 @@ describe("Sidebar", () => {
"Files",
"Skills",
"Agents",
"Plugins",
"Insights",
]);
const insights = [
Expand All @@ -315,6 +320,7 @@ describe("Sidebar", () => {
"Files",
"Skills",
"Agents",
"Plugins",
"Evals",
]);
const evals = [
Expand All @@ -333,6 +339,7 @@ describe("Sidebar", () => {
"Files",
"Skills",
"Agents",
"Plugins",
]);
act(() => root.unmount());
container.remove();
Expand Down
Loading