From 6a7e3bc362a44420504cbfe5476d481acf2ccdff Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sat, 3 Oct 2026 10:36:11 -0700 Subject: [PATCH 1/2] Add tests for first-workbench /new without roles gate --- e2e/browser/new-workbench-first-run.test.ts | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 e2e/browser/new-workbench-first-run.test.ts diff --git a/e2e/browser/new-workbench-first-run.test.ts b/e2e/browser/new-workbench-first-run.test.ts new file mode 100644 index 000000000..53086ec5e --- /dev/null +++ b/e2e/browser/new-workbench-first-run.test.ts @@ -0,0 +1,49 @@ +// CL-9844: a fresh bench lands on /new as a first-workbench onboarding step +// with no roles picker gating the composer. +import { expect, test } from "bun:test"; +import { bootBrowserApp, browserGate } from "../lib/browser"; +import { STEP_TIMEOUT, waitForText } from "../lib/first-run"; + +const describeBrowser = browserGate(import.meta.path); + +describeBrowser("new workbench first run", () => { + const app = bootBrowserApp(); + + test("zero workbenches get onboarding copy and an ungated composer", async () => { + const { page } = await app().newPage(); + try { + await page.goto(app().origin, { waitUntil: "networkidle0" }); + await page.waitForSelector(".auth-switch", { timeout: 30_000 }); + await page.click(".auth-switch"); + await page.type( + "input:not([type=password]):not([type=hidden])", + `firstrun-${String(Date.now())}@example.com`, + ); + await page.type("input[type=password]", "correct-horse-battery-staple"); + await page.click("button[type=submit]"); + + await page.waitForFunction("location.pathname === '/onboarding'", { timeout: 30_000 }); + await page.waitForSelector("button::-p-text(Skip for now)", { timeout: 30_000 }); + // A synthetic click: the step's buttons animate and pointer clicks miss. + await page.evaluate( + "Array.from(document.querySelectorAll('button')).find((b) => b.innerText.includes('Skip for now')).click()", + ); + await page.waitForFunction("location.pathname === '/new'", { timeout: 30_000 }); + + // First-run framing, not the generic empty-shell prompt. + await waitForText(page, "Create your first workbench"); + + // No roles picker: creating a workbench deploys its agent already. + const body = (await page.evaluate("document.body.innerText")) as string; + expect(body.includes("Roles for this workbench")).toBe(false); + + // The composer is usable without waiting on a roles read. + await page.waitForSelector("textarea:not([disabled])", { timeout: STEP_TIMEOUT }); + } catch (cause) { + const body = await page.evaluate("document.body.innerText"); + process.stderr.write(`new-workbench-first-run failed at ${page.url()}:\n${String(body)}\n`); + throw cause; + } + await page.close(); + }, 120_000); +}); From 2aa1735cb92ae73acf3f6537af93e807d94d07cb Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sat, 3 Oct 2026 10:39:33 -0700 Subject: [PATCH 2/2] Drop the roles picker from /new; first workbench is an onboarding step --- apps/web/src/pages/new-workbench-picker.tsx | 90 +++++---------------- apps/web/src/pages/new-workbench.css | 22 +---- apps/web/src/workbench-create.ts | 34 +------- 3 files changed, 26 insertions(+), 120 deletions(-) diff --git a/apps/web/src/pages/new-workbench-picker.tsx b/apps/web/src/pages/new-workbench-picker.tsx index 44952fbaf..36f36fc17 100644 --- a/apps/web/src/pages/new-workbench-picker.tsx +++ b/apps/web/src/pages/new-workbench-picker.tsx @@ -2,17 +2,16 @@ // posts the message so the worker starts. import { toast } from "@corbits/react-ui/ui/toast"; -import { Button, SelectionCheckbox } from "@corbits/react-ui"; import { WorkbenchLoadingState } from "@/chat"; import { Composer } from "@/chat/composer"; import { useEffect, useState } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { reportError } from "@corbits/error-sink"; import { useBench } from "../bench-context"; import { workbenchKeys } from "../chat-path"; import { NEW_WORKBENCH_TITLE, titleFromFirstMessage } from "@/auto-workbench-title"; -import { useTenantRoles } from "../agent-roles-query"; +import { listWorkbenchTenants, workbenchesQueryKey } from "@/chat/workbench-tenants"; import { createWorkbench, WorkbenchCreateError } from "../workbench-create"; import { useNavigate } from "../navigation"; import { ProviderSkipBanner } from "../provider-skip-banner"; @@ -29,9 +28,7 @@ export function describeWorkbenchCreateFailure(cause: unknown, refId?: string): const message = cause.stage === "deploy" ? "Workbench created, but its agent couldn't be deployed into it. Try again from the workbench." - : cause.stage === "roles" - ? "Workbench created, but its agent's roles couldn't be assigned yet. Open the workbench so its agent starts, then assign roles in Settings." - : GENERIC_CREATE_FAILURE; + : GENERIC_CREATE_FAILURE; return refId === undefined ? message : `${message} Reference: ${refId}`; } @@ -39,17 +36,14 @@ export function NewWorkbenchPickerRoute() { const navigate = useNavigate(); const queryClient = useQueryClient(); const { selectedTenantId } = useBench(); - // Role names, resolved against the new workbench's seeded system roles at - // create time — so only this workspace's system roles are offered here. - const roles = useTenantRoles(selectedTenantId); - const systemRoles = roles.kind === "ready" ? roles.data.filter((role) => role.isSystem) : []; - const [selectedRoles, setSelectedRoles] = useState([]); - - function toggleRole(name: string) { - setSelectedRoles((current) => - current.includes(name) ? current.filter((role) => role !== name) : [...current, name], - ); - } + // Zero workbenches means this page doubles as the onboarding step that + // creates the first one — the title says so instead of the generic prompt. + const workbenches = useQuery({ + queryKey: workbenchesQueryKey(selectedTenantId ?? ""), + enabled: selectedTenantId !== null, + queryFn: () => listWorkbenchTenants(selectedTenantId ?? ""), + }); + const isFirstWorkbench = workbenches.data?.length === 0; // Autofocus keeps the field quiet; the ring appears once Tab is used. const [keyboard, setKeyboard] = useState(false); @@ -65,17 +59,14 @@ export function NewWorkbenchPickerRoute() { mutationFn: ({ benchTenantId, openingMessage, - roles, }: { readonly benchTenantId: string; readonly openingMessage: string; - readonly roles: readonly string[]; }) => createWorkbench({ benchTenantId, name: titleFromFirstMessage(openingMessage) ?? NEW_WORKBENCH_TITLE, openingMessage, - ...(roles.length > 0 ? { roles } : {}), }), onSuccess: (tenantId, variables) => { void queryClient.invalidateQueries({ @@ -101,14 +92,11 @@ export function NewWorkbenchPickerRoute() { }); function submit(text: string) { - if (selectedTenantId === null || roles.kind !== "ready" || create.isPending) return; - create.mutate({ benchTenantId: selectedTenantId, openingMessage: text, roles: selectedRoles }); + if (selectedTenantId === null || create.isPending) return; + create.mutate({ benchTenantId: selectedTenantId, openingMessage: text }); } - // The roles read gates the composer: submitting before the options load - // (or after they fail) would create the workbench without an informed - // choice, the silent fallback role assignment forbids. - const composerDisabled = selectedTenantId === null || roles.kind !== "ready"; + const composerDisabled = selectedTenantId === null; return (
@@ -120,48 +108,14 @@ export function NewWorkbenchPickerRoute() { ) : ( <> -

What should we work on?

-
- - Roles for this workbench's agent - - {roles.kind === "loading" ? ( -

Loading workspace roles…

- ) : roles.kind === "ready" && systemRoles.length === 0 ? ( -

No system roles in this workspace yet.

- ) : roles.kind === "ready" ? ( - <> -

- System roles apply to every workbench; custom roles stay in this workspace. -

-
    - {systemRoles.map((role) => ( -
  • - toggleRole(role.name)} - rowLabel={role.name} - ariaLabel={`Assign the ${role.name} role`} - /> - {role.name} -
  • - ))} -
- - ) : roles.kind === "error" ? ( -

- Couldn't load the workspace roles — creating a workbench is blocked until - they load.{" "} - -

- ) : ( -

- Sign in again to see this workspace's roles and start a workbench. -

- )} -
+

+ {isFirstWorkbench ? "Create your first workbench" : "What should we work on?"} +

+ {isFirstWorkbench ? ( +

+ A workbench gives the job its own space and co-worker. +

+ ) : null} `. */ @@ -92,16 +86,7 @@ export async function createWorkbench(input: CreateWorkbenchInput): Promise 0) { - try { - await assignRolesToAgent({ - tenantId, - roleIds, - principalRef: { deploymentId: worker.deploymentId, address: `worker@${domain}` }, - }); - } catch (cause) { - throw failure(cause, "roles", tenantId); - } - } } catch (cause) { if (cause instanceof WorkbenchCreateError) throw cause; throw failure(cause, "deploy", tenantId);