diff --git a/apps/web/src/command-palette-actions.test.ts b/apps/web/src/command-palette-actions.test.ts index e37aaedc0..ba7bdc737 100644 --- a/apps/web/src/command-palette-actions.test.ts +++ b/apps/web/src/command-palette-actions.test.ts @@ -73,8 +73,10 @@ describe("ACTION_COMMANDS", () => { test("new-workbench and new-agent speak consumer language, not mint", () => { const workbench = ACTION_COMMANDS.find((c) => c.id === "new-workbench"); const agent = ACTION_COMMANDS.find((c) => c.id === "new-agent"); - expect(workbench?.subtitle).toBe("Start a new workbench with Myra"); - expect(agent?.subtitle).toBe("Start a new workbench with Myra"); + expect(workbench?.title).toBe("New room"); + expect(agent?.title).toBe("New room"); + expect(workbench?.subtitle).toBe("Start a new room"); + expect(agent?.subtitle).toBe("Start a new room"); expect(workbench?.subtitle.toLowerCase()).not.toContain("mint"); expect(agent?.subtitle.toLowerCase()).not.toContain("mint"); }); diff --git a/apps/web/src/command-palette-actions.ts b/apps/web/src/command-palette-actions.ts index cd7577c85..e5d88ccb1 100644 --- a/apps/web/src/command-palette-actions.ts +++ b/apps/web/src/command-palette-actions.ts @@ -95,13 +95,13 @@ export type ActionCommand = { export const ACTION_COMMANDS: readonly ActionCommand[] = [ { id: "new-workbench", - title: "New workbench", - subtitle: "Start a new workbench with Myra", + title: "New room", + subtitle: "Start a new room", }, { id: "new-agent", - title: "New workbench", - subtitle: "Start a new workbench with Myra", + title: "New room", + subtitle: "Start a new room", }, { id: "new-routine", diff --git a/apps/web/src/instant-agent-create.test.ts b/apps/web/src/instant-agent-create.test.ts index a6616da04..ebcb31540 100644 --- a/apps/web/src/instant-agent-create.test.ts +++ b/apps/web/src/instant-agent-create.test.ts @@ -104,6 +104,47 @@ describe("createWorkbenchFromTemplate (CL-6387)", () => { expect(body.name).toBe(NEW_WORKBENCH_TITLE); }); + // CL-6982: the + / picker create path mints a `kind: "workbench"` room, + // never a Myra DM clone (`kind: "chat"` + `definitionId`). + test("blank create POSTs kind=workbench — a room, not a Myra DM clone (CL-6982)", async () => { + const calls = stubFetch((path) => { + if (path.includes("/workflows/definitions")) { + return json({ data: [assistantDefinitionWire], nextCursor: null }); + } + if (path.endsWith("/chat/workbenches")) { + return json({ + id: "chan-1", + title: NEW_WORKBENCH_TITLE, + kind: "workbench", + pinned: true, + participants: [], + }); + } + throw new Error(`unexpected fetch: ${path}`); + }); + + await createWorkbenchFromTemplate( + "tnt_1", + "blank", + () => {}, + newQueryClient(), + ); + + const createCall = calls.find((call) => + call.path.endsWith("/chat/workbenches"), + ); + const body = JSON.parse(String(createCall?.init?.body)) as { + readonly kind?: string; + readonly definitionId?: string; + readonly reuseExisting?: boolean; + readonly name?: string; + }; + expect(body.kind).toBe("workbench"); + expect(body.definitionId).toBeUndefined(); + expect(body.reuseExisting).toBeUndefined(); + expect(body.name).toBe(NEW_WORKBENCH_TITLE); + }); + // CL-6387 follow-up: picking a named template threw its own name away // and left the reviewer roster its greeting promises out of the room // (every bench looked like every other "New Workbench", and Myra's diff --git a/apps/web/src/instant-agent-create.ts b/apps/web/src/instant-agent-create.ts index 538880f8f..cbcd5d34d 100644 --- a/apps/web/src/instant-agent-create.ts +++ b/apps/web/src/instant-agent-create.ts @@ -1,17 +1,14 @@ -// Every "create a workbench" affordance — the sidebar's "+", the command -// palette's "New workbench", and the zero-workbench land-hop on `/` +// Every "create a room" affordance — the sidebar's "+", the command +// palette's "New room", and the zero-workbench land-hop on `/` // (CL-6486, superseding CL-6138's silent auto-mint) — opens the template // picker (`pages/new-workbench-picker.tsx`, CL-6342) and calls // `createWorkbenchFromTemplate` below once a row is chosen. It mints a -// fresh workbench against the account's default setup template (the same -// seeded `assistant` definition backing the home Myra workbench, which -// already opens with the setup greeting: "what do you want me around -// for?"). The conversation itself is what specializes the agent into -// whatever the person wants; the drafting and capability machinery already -// listens for that in-chat, so no definition is drafted or created up -// front here. Explicitly defining a brand-new agent template, with its own -// name/purpose/model/skills chosen up front, stays `CreateAgentPanel`'s job -// (Settings → Agents), unchanged. +// fresh `kind: "workbench"` room (CL-6982), not a Myra DM clone +// (`kind: "chat"` + `definitionId`). Named templates still instantiate +// their participant roster after the room exists. Explicitly defining a +// brand-new agent template, with its own name/purpose/model/skills +// chosen up front, stays `CreateAgentPanel`'s job (Settings → Agents), +// unchanged. import { getLogger } from "@corbits/client-log"; import type { QueryClient } from "@tanstack/react-query"; @@ -44,7 +41,7 @@ import { workbenchPath } from "./workbench-path"; const log = getLogger("web.instant-agent-create"); import type { WorkbenchTemplateId } from "./workbench-templates"; -export const NEW_WORKBENCH_TITLE = "New Workbench"; +export const NEW_WORKBENCH_TITLE = "New room"; /** * Marks the two precondition failures below as intentionally @@ -98,18 +95,15 @@ export type PickGithubRepos = (args: { }) => Promise; /** - * The template picker's "Create workbench" action (CL-6344): mints a - * fresh chat, named after the picked template, against the account's - * default setup template (the seeded `assistant`/Myra definition), - * passing the picked row's id through as `templateId` so the room opens - * with that template's own intro (`packages/chat/src/routes.ts`'s - * `POST /workbenches` resolves it into the canned greeting). When the id - * names a real manifest (`workbenchTemplate`), this also creates its - * participant agent definitions, invites each into the room so the - * roster the greeting promises is the roster actually there (see + * The template picker's create action (CL-6344, CL-6982): mints a fresh + * `kind: "workbench"` room, named after the picked template, with no + * `definitionId` — a room, not a Myra DM clone. When the id names a real + * manifest (`workbenchTemplate`), this also creates its participant agent + * definitions, invites each into the room so the roster the greeting + * promises is the roster actually there (see * `instantiateWorkbenchTemplate`'s own doc), and records its required * connections as pending. A template id with no manifest yet (`blank`, - * "Just start talking") mints a plain untagged chat under the generic + * "Just start talking") mints a plain untagged room under the generic * `NEW_WORKBENCH_TITLE`, exactly like before templates existed — there * is no better name to give it. When `pickGithubRepos` is supplied and * GitHub is already connected for this tenant, this also drives @@ -141,8 +135,7 @@ export async function createWorkbenchFromTemplate( firstMessage?: string, ): Promise { const definitions = await listAgentDefinitions(tenantId); - const setupTemplate = findMyraDefinition(definitions); - if (setupTemplate === undefined) { + if (findMyraDefinition(definitions) === undefined) { throw new WorkbenchPreconditionError( SETUP_AGENT_MISSING_MESSAGE, "setup-agent-missing", @@ -183,13 +176,8 @@ export async function createWorkbenchFromTemplate( : false; const workbench = await createWorkbench(tenantId, { - kind: "chat", - definitionId: setupTemplate.id, + kind: "workbench", name: manifest?.title ?? NEW_WORKBENCH_TITLE, - ...(manifest !== undefined ? { templatePromise: manifest.promise } : {}), - ...(requiresGithub && !githubAlreadyConnected - ? { connectGithubRequiredFor: manifest?.title ?? "" } - : {}), }); if (githubAlreadyConnected && pickGithubRepos !== undefined) { diff --git a/apps/web/src/pages/new-workbench-picker.tsx b/apps/web/src/pages/new-workbench-picker.tsx index 608be6706..658607079 100644 --- a/apps/web/src/pages/new-workbench-picker.tsx +++ b/apps/web/src/pages/new-workbench-picker.tsx @@ -216,7 +216,7 @@ export function NewWorkbenchPickerRoute() { return (
, render: () => , hasStageTopBar: false, @@ -268,7 +268,7 @@ export const APP_ROUTES: readonly AppRoute[] = [ }, { path: NEW_WORKBENCH_PATH, - label: "New workbench", + label: "New room", icon: , render: () => , }, diff --git a/apps/web/src/shell/sidebar.tsx b/apps/web/src/shell/sidebar.tsx index befd02d45..8560a1750 100644 --- a/apps/web/src/shell/sidebar.tsx +++ b/apps/web/src/shell/sidebar.tsx @@ -153,8 +153,8 @@ export function Sidebar({ - ); -} - /** * The ellipsis-menu item labels for a workbench row: rename and the * pin/unpin toggle — a strict subset of the conversation's own settings. @@ -404,10 +374,11 @@ export function WorkbenchList({ return (
-

Workbenches

-
- -
+ } + title="No workbenches yet" + description="Start a new one with the + above." + />
); } diff --git a/apps/web/src/workbench-templates.ts b/apps/web/src/workbench-templates.ts index be3d1f8c0..b144c2093 100644 --- a/apps/web/src/workbench-templates.ts +++ b/apps/web/src/workbench-templates.ts @@ -34,7 +34,6 @@ export const WORKBENCH_TEMPLATES: readonly WorkbenchTemplate[] = [ { id: "blank", title: "Just start talking", - promise: - "An empty room with Myra in it. Bring your own work, connect things as you go.", + promise: "An empty room. Bring your own work, connect things as you go.", }, ]; diff --git a/apps/web/test/new-workbench-picker.test.tsx b/apps/web/test/new-workbench-picker.test.tsx index 7d9c00910..3e45d4a9b 100644 --- a/apps/web/test/new-workbench-picker.test.tsx +++ b/apps/web/test/new-workbench-picker.test.tsx @@ -346,9 +346,11 @@ describe("NewWorkbenchPickerRoute", () => { call.path.endsWith("/chat/workbenches") && call.init?.method === "POST", ); expect(JSON.parse(String(createWorkbenchCall?.init?.body))).toMatchObject({ - kind: "chat", - definitionId: "wfd_assistant", + kind: "workbench", }); + expect( + JSON.parse(String(createWorkbenchCall?.init?.body)).definitionId, + ).toBeUndefined(); const sendMessageCall = calls.find((call) => call.path.endsWith("/chat/workbenches/chan_new/messages"), @@ -543,11 +545,14 @@ describe("NewWorkbenchPickerRoute", () => { call.path.endsWith("/chat/workbenches") && call.init?.method === "POST", ); expect(JSON.parse(String(createWorkbenchCall?.init?.body))).toMatchObject({ - kind: "chat", - definitionId: "wfd_assistant", - templatePromise: - "Three reviewers read every pull request and post what they'd change.", + kind: "workbench", }); + expect( + JSON.parse(String(createWorkbenchCall?.init?.body)).definitionId, + ).toBeUndefined(); + expect( + JSON.parse(String(createWorkbenchCall?.init?.body)).templatePromise, + ).toBeUndefined(); expect(createdAgentHandles).toEqual([ "correctness-reviewer", diff --git a/apps/web/test/routes.test.tsx b/apps/web/test/routes.test.tsx index 2a30bcd42..c2d1121dc 100644 --- a/apps/web/test/routes.test.tsx +++ b/apps/web/test/routes.test.tsx @@ -327,7 +327,7 @@ describe("routes render", () => { const markup = await renderApp(route.path); expect(markup).toContain('data-testid="shell-sidebar"'); if (route.path === "/") { - expect(stagePageTitle(markup)).toBe("New Workbench"); + expect(stagePageTitle(markup)).toBe("New room"); return; } if (route.path === "/w") { diff --git a/apps/web/test/sidebar.test.tsx b/apps/web/test/sidebar.test.tsx index b86abb03d..7927ff57e 100644 --- a/apps/web/test/sidebar.test.tsx +++ b/apps/web/test/sidebar.test.tsx @@ -165,7 +165,7 @@ async function mountSidebar( describe("Sidebar", () => { test("header offers create + search; there is no collapse affordance", () => { const markup = renderSidebar("/w"); - expect(markup).toContain('aria-label="New workbench"'); + expect(markup).toContain('aria-label="New room"'); // Search is the box inside the list (below the brand row), never a // header icon — the box itself is covered by the workbench-list tests. expect(markup).not.toContain('aria-label="Search"'); @@ -197,7 +197,7 @@ describe("Sidebar", () => { }); const newButton = container.querySelector( - '[aria-label="New workbench"]', + '[aria-label="New room"]', ); await act(async () => { newButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); @@ -576,11 +576,10 @@ describe("Sidebar", () => { container.remove(); }); - // CL-6124: a bench with zero workbenches lands on the first-run chat - // (`/`), and the sidebar names it as a single active row — never the - // icon "No workbenches yet" empty state, since the create-a-workbench - // surface IS this screen now. - test("zero workbenches: a single New Workbench row, styled active, not an icon empty state", async () => { + // CL-6982: a bench with zero workbenches shows the honest empty state — + // never a fake "New Workbench" row that looked like a clone already + // existed. The + above is the mint verb ("New room"). + test("zero workbenches: honest empty state, no stub New Workbench row", async () => { stubFetch(); const container = document.createElement("div"); document.body.appendChild(container); @@ -598,12 +597,13 @@ describe("Sidebar", () => { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); - if (container.innerHTML.includes("shell-ch-row")) break; + if (container.innerHTML.includes("No workbenches yet")) break; } - const row = container.querySelector('.shell-ch-row[data-active="true"]'); - expect(row).not.toBeNull(); - expect(row?.textContent).toContain("New Workbench"); - expect(container.innerHTML).not.toContain("No workbenches yet"); + expect(container.innerHTML).toContain("No workbenches yet"); + expect(container.innerHTML).not.toContain("New Workbench"); + expect( + container.querySelector('.shell-ch-row[data-active="true"]'), + ).toBeNull(); act(() => root.unmount()); container.remove(); }); diff --git a/packages/chat-ui/src/api.ts b/packages/chat-ui/src/api.ts index a037bf9ab..74eea6b2d 100644 --- a/packages/chat-ui/src/api.ts +++ b/packages/chat-ui/src/api.ts @@ -339,7 +339,17 @@ export function listAllWorkbenches( // find-or-create caller: the home-workbench land-hop // (`default-agent-workbench.ts`'s `ensure`). export type CreateWorkbenchInput = - | { readonly kind: "workbench"; readonly name: string } + | { + readonly kind: "workbench"; + readonly name: string; + /** The template's own display name, present exactly when the + * template needs a GitHub connection before it can run — posts one + * `connect-github` block on the empty room + * (`packages/chat/src/routes.ts`'s `POST /workbenches`). Omitted + * for a template with no such requirement, or when GitHub is + * already connected and the create flow picks repos inline. */ + readonly connectGithubRequiredFor?: string; + } | { readonly kind: "chat"; readonly definitionId: string; diff --git a/packages/workflow-catalog/src/instantiate.ts b/packages/workflow-catalog/src/instantiate.ts index 68d6d8ca1..500c7e28a 100644 --- a/packages/workflow-catalog/src/instantiate.ts +++ b/packages/workflow-catalog/src/instantiate.ts @@ -76,8 +76,9 @@ export interface WorkbenchTemplateInstantiationPorts { * `@corbits/chat-ui`'s `inviteAgent`), or a fake of it in tests. This * is what makes a template's roster actually present in the room * rather than merely registered in the agent directory. Never called - * for Myra: she joins the room at workbench creation as its own - * `definitionId`. */ + * for Myra: rooms do not auto-host her (CL-6982), and a template that + * lists her handle is skipped rather than inviting her into an empty + * room. */ inviteParticipantAgent(id: string): Promise; /** Persists the room's still-needed connections — the workbench * settings `template/pendingConnections` key today; see @@ -127,11 +128,11 @@ function webhookTriggerTodo( /** * Resolves `manifest` against the bench: creates the participant agent - * definitions that don't already exist (Myra is never re-created — she - * is the bench's seeded default setup agent, reused as-is), and - * records the manifest's required connections as still pending. Never - * registers a live webhook trigger — see `webhookTriggerTodos` and - * `./connect-github-setup.ts`, which is what actually creates one, once + * definitions that don't already exist (Myra is never created or invited — + * rooms do not auto-host her; templates that list other agents still invite + * those agents), and records the manifest's required connections as still + * pending. Never registers a live webhook trigger — see `webhookTriggerTodos` + * and `./connect-github-setup.ts`, which is what actually creates one, once * the person has picked repos. */ export async function instantiateWorkbenchTemplate( diff --git a/packages/workflow-catalog/test/instantiate.test.ts b/packages/workflow-catalog/test/instantiate.test.ts index e1cf21282..ce61238f4 100644 --- a/packages/workflow-catalog/test/instantiate.test.ts +++ b/packages/workflow-catalog/test/instantiate.test.ts @@ -112,7 +112,10 @@ test("instantiating the code-review template invites every reviewer into the roo expect(ports.invited).toHaveLength(3); }); -test("instantiating the code-review template never invites Myra — she is already the room's own agent", async () => { +// Rooms do not auto-host Myra (CL-6982). Instantiation skips her handle +// rather than inviting her into an empty room; other template agents +// still get invited. +test("instantiating the code-review template never invites Myra — rooms do not auto-host her", async () => { const ports = fakePorts(); await instantiateWorkbenchTemplate(CODE_REVIEW_TEMPLATE, ports); expect(ports.invited).not.toContain("def-myra");