From b2a5a22da4c5f52c6bf5df1dfb1181ccd45fff16 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 21:49:51 -0700 Subject: [PATCH] feat(web): sidebar nav is Artifacts, Skills, Tools, Workflows; Insights and account live under Settings (CL-8427) Renames Routines to Workflows and Files to Artifacts everywhere a person sees them, with old paths redirecting. Insights and the account menu move under Settings; the sidebar footer collapses to the four primary destinations plus a single Settings row. Retires the dormant, never-linked workflow-definition-detail scaffolding at /workflows/:id that squatted on the path the Workflows rename now needs. --- apps/web/src/chat/artifact-chip.tsx | 6 +- apps/web/src/chat/strings.ts | 2 +- apps/web/src/command-palette-actions.test.ts | 8 +- apps/web/src/command-palette-actions.ts | 8 +- apps/web/src/command-palette-provider.tsx | 18 +- apps/web/src/global-routines.ts | 8 +- apps/web/src/last-workbench.ts | 2 +- apps/web/src/library-upload.ts | 2 +- apps/web/src/library/kind-filter.test.ts | 28 +- apps/web/src/library/kind-filter.ts | 19 +- apps/web/src/login-routing.test.tsx | 16 +- .../src/pages/legacy-settings-redirects.tsx | 8 +- apps/web/src/pages/library-page.tsx | 57 ++-- apps/web/src/pages/routine-detail-page.tsx | 182 ++++++++++- apps/web/src/pages/routines-page.tsx | 8 +- apps/web/src/pages/workflow-detail-page.tsx | 293 ------------------ apps/web/src/path-ids.ts | 18 +- apps/web/src/routes.tsx | 103 +++--- apps/web/src/settings-groups.ts | 17 - apps/web/src/settings-groups.tsx | 26 ++ apps/web/src/settings/account-section.tsx | 22 +- apps/web/src/shell/app-shell.tsx | 2 +- apps/web/src/shell/canvas-availability.tsx | 6 +- .../web/src/shell/context-menu/items.test.tsx | 4 +- apps/web/src/shell/context-menu/items.tsx | 2 +- apps/web/src/shell/library-artifacts.ts | 6 +- apps/web/src/shell/shell-chrome-provider.tsx | 4 +- apps/web/src/shell/sidebar.tsx | 173 +++-------- apps/web/src/workflow-detail-api.ts | 111 ------- 29 files changed, 429 insertions(+), 730 deletions(-) delete mode 100644 apps/web/src/pages/workflow-detail-page.tsx delete mode 100644 apps/web/src/settings-groups.ts create mode 100644 apps/web/src/settings-groups.tsx delete mode 100644 apps/web/src/workflow-detail-api.ts diff --git a/apps/web/src/chat/artifact-chip.tsx b/apps/web/src/chat/artifact-chip.tsx index b247d5b569..0a90734044 100644 --- a/apps/web/src/chat/artifact-chip.tsx +++ b/apps/web/src/chat/artifact-chip.tsx @@ -7,7 +7,7 @@ // // Opening a chip is a callback the host supplies (mirrors `onOpenProfile` // and `onOpenThread` in `timeline.tsx`): this package owns no router. A -// second, artifactId-only affordance — "Open in Files" — hands the host a +// second, artifactId-only affordance — "Open in Artifacts" — hands the host a // separate callback so it can navigate there directly; it only // ever renders when `artifactId` is present, since a blob-only part has no // Library row to deep-link to. @@ -52,8 +52,8 @@ export function ArtifactChip({ - ) : null} + {onSignOut !== undefined ? ( + + ) : null} +
diff --git a/apps/web/src/shell/app-shell.tsx b/apps/web/src/shell/app-shell.tsx index 19376671c4..eb3309ab86 100644 --- a/apps/web/src/shell/app-shell.tsx +++ b/apps/web/src/shell/app-shell.tsx @@ -133,7 +133,7 @@ export function AppShell({ return (
- +
{routeHasNoStageTopBar(path) ? ( diff --git a/apps/web/src/shell/canvas-availability.tsx b/apps/web/src/shell/canvas-availability.tsx index c2e00eb2f5..98878c0f65 100644 --- a/apps/web/src/shell/canvas-availability.tsx +++ b/apps/web/src/shell/canvas-availability.tsx @@ -45,7 +45,7 @@ export type RoutinePanelSubject = { * brand-new one (`routineId` omitted or `null`) — routines-page's own * "New routine"/"Edit" actions, "Make this a routine", the composer's * `/routine` command, and "New routine in this space" (: - * browsing/running existing routines moved to the global `/routines` + * browsing/running existing routines moved to the global `/workflows` * page, so this pane no longer has a list mode). */ readonly routineId?: string | null; /** Seeds the Name/Instruction fields the instant a brand-new panel opens @@ -60,7 +60,7 @@ export type RoutinePanelSubject = { * routine, and its own id is where the routine delivers. Carried through * list mode too, so "New routine" picked from the list still binds to * the workbench the panel was opened beside. Omitted only when there is no - * open conversation to bind to (e.g. a deliberate `/routines` visit), + * open conversation to bind to (e.g. a deliberate `/workflows` visit), * in which case the panel falls back to this workbench's own default * (Myra) workbench — never mints a new one. */ readonly workbenchId?: string; @@ -207,7 +207,7 @@ export function useOpenArtifactInCanvas(): (artifact: CanvasArtifactContent) => } /** Opens (or replaces) the canvas's routine pane — the workbench header's - * "New routine" action, the `/routines` page's own create button, and an + * "New routine" action, the `/workflows` page's own create button, and an * existing routine's own "Edit" hop all call this. `routineId: null` starts * a brand-new routine; a real id opens that routine for editing. */ export function useOpenRoutineInCanvas(): (subject: RoutinePanelSubject) => void { diff --git a/apps/web/src/shell/context-menu/items.test.tsx b/apps/web/src/shell/context-menu/items.test.tsx index a6d996c30f..26deee27b6 100644 --- a/apps/web/src/shell/context-menu/items.test.tsx +++ b/apps/web/src/shell/context-menu/items.test.tsx @@ -174,7 +174,7 @@ describe("shellContextMenuFor: routine", () => { const navigate = mock((_to: string) => undefined); const menu = shellContextMenuFor(target, actions({ navigate })); findItem(menu.entries, "open").onSelect(); - expect(navigate).toHaveBeenCalledWith("/routines/rt-1"); + expect(navigate).toHaveBeenCalledWith("/workflows/rt-1"); }); // `runScheduledWorkflowNow` no longer calls a hub route at all @@ -255,7 +255,7 @@ describe("shellContextMenuFor: artifact", () => { await Promise.resolve(); await Promise.resolve(); expect(navigator.clipboard.writeText).toHaveBeenCalledWith( - `${window.location.origin}/files/a/art_1\n${window.location.origin}/files/a/art_2`, + `${window.location.origin}/artifacts/a/art_1\n${window.location.origin}/artifacts/a/art_2`, ); expect(toastMock).toHaveBeenCalledWith("2 links copied"); }); diff --git a/apps/web/src/shell/context-menu/items.tsx b/apps/web/src/shell/context-menu/items.tsx index 953ebd8747..f45e9bbee8 100644 --- a/apps/web/src/shell/context-menu/items.tsx +++ b/apps/web/src/shell/context-menu/items.tsx @@ -139,7 +139,7 @@ function routineMenu( target: Extract, actions: ShellContextMenuActions, ): ContextMenu { - const path = `/routines/${encodeURIComponent(target.id)}`; + const path = `/workflows/${encodeURIComponent(target.id)}`; const entries: ContextMenuEntry[] = [ contextMenuItem({ id: "open", diff --git a/apps/web/src/shell/library-artifacts.ts b/apps/web/src/shell/library-artifacts.ts index 7acf492807..5f5b725ac0 100644 --- a/apps/web/src/shell/library-artifacts.ts +++ b/apps/web/src/shell/library-artifacts.ts @@ -10,7 +10,7 @@ import type { ArtifactSummary } from "@/library"; import { ApiQueryError, UnauthenticatedError } from "@/lib/api-query"; -import { FILES_PATH_PREFIX } from "../path-ids"; +import { ARTIFACTS_PATH_PREFIX } from "../path-ids"; /** List row from the hub artifacts surface (content omitted). */ export type ArtifactListRow = { @@ -151,9 +151,9 @@ export function artifactUploadToast(names: readonly string[]): string { */ export const LIBRARY_BULK_OPERATION_IDS = ["copy-link"] as const; -/** `/files/a/:id` — the one canonical deep link a file has. */ +/** `/artifacts/a/:id` — the one canonical deep link a file has. */ export function libraryArtifactDeepLink(id: string): string { - return `${FILES_PATH_PREFIX}/a/${encodeURIComponent(id)}`; + return `${ARTIFACTS_PATH_PREFIX}/a/${encodeURIComponent(id)}`; } /** Copies one or more files' canonical links, newline-joined, to the diff --git a/apps/web/src/shell/shell-chrome-provider.tsx b/apps/web/src/shell/shell-chrome-provider.tsx index 1733a7ce19..feaefba6c7 100644 --- a/apps/web/src/shell/shell-chrome-provider.tsx +++ b/apps/web/src/shell/shell-chrome-provider.tsx @@ -33,8 +33,8 @@ import { } from "./canvas-availability"; /** First pathname segment, ignoring query and hash. Nested detail under the - * same surface (`/routines` vs `/routines/:id`) shares a prefix; a rail leave - * (`/routines` → `/insights`) does not. */ + * same surface (`/workflows` vs `/workflows/:id`) shares a prefix; a rail leave + * (`/workflows` → `/insights`) does not. */ function inAppRoutePrefix(path: string): string { const pathname = path.split("?")[0]?.split("#")[0] ?? ""; const trimmed = pathname.startsWith("/") ? pathname.slice(1) : pathname; diff --git a/apps/web/src/shell/sidebar.tsx b/apps/web/src/shell/sidebar.tsx index f5dff8c157..4d397032c1 100644 --- a/apps/web/src/shell/sidebar.tsx +++ b/apps/web/src/shell/sidebar.tsx @@ -1,10 +1,9 @@ // 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 -// joins only when the existing 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 settings, feedback, and log out. +// primary rail is Artifacts, Skills, Tools, Workflows, Agents. Below the +// rail: a single Settings row (icon + label) — Insights, the account menu +// (avatar, name, sign out), and everything else now live inside Settings +// itself, not as separate sidebar affordances. // Always present; there is no collapse affordance and no second nav column. // Approvals belong in the conversation, not as a standing band here. // @@ -19,66 +18,30 @@ // (`command-palette-actions.ts`), which only appears once memberships // resolve to more than one workbench. +import { Button, SidebarPanel, SidebarPanelBody, SidebarPanelFooter } from "@corbits/react-ui"; import { - Avatar, - Button, - Menu, - MenuContent, - MenuItem, - MenuSeparator, - MenuTrigger, - SidebarPanel, - SidebarPanelBody, - SidebarPanelFooter, -} from "@corbits/react-ui"; -import { - ChartBar, - ChatCircleDots, + FlowArrow, FolderOpen, Lightning, + Plugs, Plus, Robot, - SignOut, - Repeat, SlidersHorizontal, SquaresFour, } from "@/lib/icons"; -import { avatarClassForPrincipal } from "@/chat"; - -import webPackage from "../../package.json"; -import { useAPIQuery } from "../api"; -import { useBench } from "../bench-context"; -import { TopLevelRunsSchema, insightsTopLevelRunsPath } from "../insights-api"; -import { matchesRoute, MISSION_CONTROL_PATH, SETTINGS_PATH } from "../routes"; import { NEW_CHAT_PATH } from "../chat-path"; -import type { SessionUser } from "../session"; +import { matchesRoute, MISSION_CONTROL_PATH, SETTINGS_PATH } from "../routes"; import { SidebarBrandMark } from "./brand-mark"; -import { initialsOf } from "./docks"; import { WorkbenchList } from "./workbench-list"; -/** The repo's own issue tracker — read off this package's manifest (set - * from `git remote`) rather than a hardcoded org/repo guess. */ -const FEEDBACK_URL = `${webPackage.repository.url}/issues`; - export function Sidebar({ path, - user, onNavigate, - onSignOut, }: { readonly path: string; - readonly user: SessionUser; readonly onNavigate: (to: string) => void; - readonly onSignOut: () => void; }) { - const { selectedTenantId } = useBench(); - const runsQuery = useAPIQuery( - selectedTenantId === null ? "" : insightsTopLevelRunsPath(selectedTenantId), - TopLevelRunsSchema, - ); - const showInsights = runsQuery.kind === "ready" && runsQuery.data.data.length > 0; - return ( - + + - {showInsights ? ( - - ) : null} -
- - - - - - - - Send Feedback - - - - - Log out - - - - -
+
); diff --git a/apps/web/src/workflow-detail-api.ts b/apps/web/src/workflow-detail-api.ts deleted file mode 100644 index a332e772f8..0000000000 --- a/apps/web/src/workflow-detail-api.ts +++ /dev/null @@ -1,111 +0,0 @@ -// The workflow detail page's one seam to reading a definition. There is -// no `GET .../:id` route, stock or otherwise -// (`vendor/intx/hub-api/src/routes/workflow-definitions.ts` only lists, -// lists versions, and rolls back), so this walks the stock list a page -// at a time and stops at the matching id. Wire schema and pure display -// helpers live in -// `@corbits/workflows/client`, browser-safe like `routines-api.ts`'s own -// definitions listing — this file is fetch composition only. -import { type } from "arktype"; -import type { ArkErrors } from "arktype"; -import { ApiQueryError, UnauthenticatedError } from "@/lib/api-query"; -import { WorkflowDefinitionDetail } from "@corbits/workflows/client"; -import type { WorkflowDefinitionDetail as WorkflowDefinitionDetailT } from "@corbits/workflows/client"; - -export type { WorkflowDefinitionDetail as WorkflowDefinitionDetailT } from "@corbits/workflows/client"; -export { workflowDetailPath, workflowNotLaunchableReason } from "@corbits/workflows/client"; - -const StockWorkflowDefinition = type({ - id: "string", - "description?": "string | null", - name: "string", - currentVersion: "string", - status: "'deployed' | 'stopped'", - createdAt: "string", - updatedAt: "string", -}); - -const StockWorkflowDefinitionsPage = type({ - data: StockWorkflowDefinition.array(), - nextCursor: "string | null", -}); - -type Validator = (data: unknown) => T | ArkErrors; - -async function request(path: string, schema: Validator): Promise { - let response: Response; - try { - response = await fetch(path, { - headers: { "content-type": "application/json" }, - }); - } catch (cause) { - throw new ApiQueryError( - cause instanceof Error ? cause.message : String(cause), - undefined, - path, - ); - } - if (response.status === 401) { - throw new UnauthenticatedError(); - } - if (!response.ok) { - const detail = await response - .json() - .then( - (body: { error?: { userMessage?: string; message?: string } }) => - body.error?.userMessage ?? body.error?.message ?? "", - ) - .catch(() => ""); - throw new ApiQueryError( - detail === "" ? `The server answered ${response.status}.` : detail, - response.status, - path, - ); - } - const body: unknown = await response.json().catch(() => undefined); - const parsed = schema(body); - if (parsed instanceof type.errors) { - throw new ApiQueryError(`Unexpected response shape: ${parsed.summary}`, undefined, path); - } - return parsed; -} - -const MAX_PAGES_SCANNED = 20; - -export async function getWorkflowDefinitionDetail( - tenantId: string, - definitionId: string, -): Promise { - let cursor: string | null | undefined; - for (let page = 0; page < MAX_PAGES_SCANNED; page += 1) { - const query = cursor != null ? `?cursor=${encodeURIComponent(cursor)}` : ""; - const listPath = `/api/tenants/${tenantId}/workflows/definitions${query}`; - const body = await request(listPath, StockWorkflowDefinitionsPage); - const found = body.data.find((item) => item.id === definitionId); - if (found !== undefined) { - const detail: WorkflowDefinitionDetailT = { - definitionId: found.id, - name: found.name, - status: found.status, - currentVersion: found.currentVersion, - createdAt: found.createdAt, - updatedAt: found.updatedAt, - ...(found.description !== undefined && found.description !== null - ? { description: found.description } - : {}), - }; - const parsed = WorkflowDefinitionDetail(detail); - if (parsed instanceof type.errors) { - throw new ApiQueryError( - `Unexpected response shape: ${parsed.summary}`, - undefined, - listPath, - ); - } - return parsed; - } - if (body.nextCursor == null) break; - cursor = body.nextCursor; - } - throw new ApiQueryError("Workflow not found.", 404, definitionId); -}