From 0adda27a5c05ea138a5f0225de35d3c63b55c907 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 23 Aug 2026 23:54:00 -0700 Subject: [PATCH 1/3] CL-6982: + creates kind=workbench room, not a Myra DM clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mint a blank kind=workbench room (New room copy) with no definitionId and no stub New Workbench sidebar row. User-facing copy says room not channel — vocab cutover is CL-6977. --- apps/web/src/command-palette-actions.test.ts | 6 ++- apps/web/src/command-palette-actions.ts | 8 ++-- apps/web/src/instant-agent-create.test.ts | 41 +++++++++++++++++ apps/web/src/instant-agent-create.ts | 48 ++++++++------------ apps/web/src/pages/new-workbench-picker.tsx | 2 +- apps/web/src/routes.tsx | 6 +-- apps/web/src/shell/sidebar.tsx | 4 +- apps/web/src/shell/workbench-list.tsx | 39 ++-------------- apps/web/test/new-workbench-picker.test.tsx | 17 ++++--- apps/web/test/routes.test.tsx | 2 +- apps/web/test/sidebar.test.tsx | 22 ++++----- 11 files changed, 100 insertions(+), 95 deletions(-) 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/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..73c94f3b3 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,11 @@ 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(); }); From 69c5f5991af9b62295f569642d81edc14f2932c2 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 09:30:11 -0700 Subject: [PATCH 2/3] Format files changed in this PR --- apps/web/src/instant-agent-create.ts | 76 +++++++------------ apps/web/src/workbench-templates.ts | 2 +- apps/web/test/sidebar.test.tsx | 4 +- packages/chat-ui/src/api.ts | 12 ++- packages/workflow-catalog/src/instantiate.ts | 15 ++-- .../workflow-catalog/test/instantiate.test.ts | 5 +- 6 files changed, 56 insertions(+), 58 deletions(-) diff --git a/apps/web/src/instant-agent-create.ts b/apps/web/src/instant-agent-create.ts index cbcd5d34d..a9dd2a4ba 100644 --- a/apps/web/src/instant-agent-create.ts +++ b/apps/web/src/instant-agent-create.ts @@ -4,8 +4,9 @@ // picker (`pages/new-workbench-picker.tsx`, CL-6342) and calls // `createWorkbenchFromTemplate` below once a row is chosen. It mints a // 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 +// (`kind: "chat"` + `definitionId`). Rooms do not auto-host Myra: a blank +// mint is an empty room; named templates still instantiate their +// non-Myra 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. @@ -35,7 +36,6 @@ import { } from "./workbench-templates-api"; import { createAgentDefinition, listAgentDefinitions } from "./agents-api"; -import { findMyraDefinition } from "./myra-workbench"; import { workbenchPath } from "./workbench-path"; const log = getLogger("web.instant-agent-create"); @@ -44,22 +44,17 @@ import type { WorkbenchTemplateId } from "./workbench-templates"; export const NEW_WORKBENCH_TITLE = "New room"; /** - * Marks the two precondition failures below as intentionally - * user-facing: their `message` is authored copy, never a raw request - * path or schema summary, so a caller can show it verbatim. Every - * other throw on this path (`ApiQueryError`, `ChatApiError`, or a - * plain `Error` from a package that hasn't opted in) must go through - * that error type's own describer instead — allow-listing safe - * throws, rather than denylisting unsafe ones, so a new error type - * added later fails safe (masked) instead of leaking by default. + * Marks the precondition failure below as intentionally user-facing: its + * `message` is authored copy, never a raw request path or schema summary, + * so a caller can show it verbatim. Every other throw on this path + * (`ApiQueryError`, `ChatApiError`, or a plain `Error` from a package that + * hasn't opted in) must go through that error type's own describer instead + * — allow-listing safe throws, rather than denylisting unsafe ones, so a + * new error type added later fails safe (masked) instead of leaking by + * default. * - * `kind` lets a caller tell "the setup agent isn't deployed yet" apart - * from "this template genuinely doesn't exist here" without parsing - * `message` text: the first is very often a still-provisioning bench - * (CL-6457's background deploy hasn't finished, or never started - * without a credential) that the caller should check - * `fetchAgentReadiness` over before treating as a dead end; the second - * never resolves itself and should surface as-is. + * `kind` lets a caller tell "this template genuinely doesn't exist here" + * apart from other failures without parsing `message` text. */ export class WorkbenchPreconditionError extends Error { readonly kind: "setup-agent-missing" | "template-unavailable"; @@ -72,15 +67,6 @@ export class WorkbenchPreconditionError extends Error { } } -/** - * Consumer-language stand-in for the system precondition this bench - * hit: "no deployed setup agent" describes an internal implementation - * detail, never something a person signing in for the first time - * should have to parse. - */ -const SETUP_AGENT_MISSING_MESSAGE = - "Your workbench is still finishing setup. Try again in a moment."; - /** * Presents the connected org's repo list for the person to pick from once * the workbench exists — the create flow's own "select" half of CL-6386 @@ -97,18 +83,19 @@ export type PickGithubRepos = (args: { /** * 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 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 + * `definitionId` — a room, not a Myra DM clone, and not auto-hosting Myra. + * When the id names a real manifest (`workbenchTemplate`), this also + * creates its non-Myra 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 empty room under the + * generic `NEW_WORKBENCH_TITLE`. When `pickGithubRepos` is supplied and * GitHub is already connected for this tenant, this also drives * CL-6386's "select on new-workbench" step — see `PickGithubRepos`'s - * own doc. + * own doc. When GitHub is still required and not yet connected, the mint + * carries `connectGithubRequiredFor` so the room opens with the + * connect-github card. * * `queryClient` invalidates the workbenches list once every template * participant has been invited (CL-6594) — `ChatWorkspace`'s own @@ -122,9 +109,8 @@ export type PickGithubRepos = (args: { * * `firstMessage`, when given (CL-6628's prompt box), is sent as the * signed-in person's own opening message once the room and its - * template participants exist, so it lands after the setup/template - * greeting rather than racing it — Myra reads the room's actual intent - * as the next line, not the first. + * template participants exist, so it lands after any template + * greeting rather than racing it. */ export async function createWorkbenchFromTemplate( tenantId: string, @@ -134,13 +120,6 @@ export async function createWorkbenchFromTemplate( pickGithubRepos?: PickGithubRepos, firstMessage?: string, ): Promise { - const definitions = await listAgentDefinitions(tenantId); - if (findMyraDefinition(definitions) === undefined) { - throw new WorkbenchPreconditionError( - SETUP_AGENT_MISSING_MESSAGE, - "setup-agent-missing", - ); - } // The manifest comes from the bench library (CL-6344), never from a // hardcoded catalog import; reading it is what seeds the shelf // (CL-6458). `blank` is the one id with no manifest by design; any @@ -178,6 +157,9 @@ export async function createWorkbenchFromTemplate( const workbench = await createWorkbench(tenantId, { kind: "workbench", name: manifest?.title ?? NEW_WORKBENCH_TITLE, + ...(requiresGithub && !githubAlreadyConnected + ? { connectGithubRequiredFor: manifest?.title ?? "" } + : {}), }); if (githubAlreadyConnected && pickGithubRepos !== undefined) { diff --git a/apps/web/src/workbench-templates.ts b/apps/web/src/workbench-templates.ts index be3d1f8c0..4590320df 100644 --- a/apps/web/src/workbench-templates.ts +++ b/apps/web/src/workbench-templates.ts @@ -35,6 +35,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.", + "An empty room. Bring your own work, connect things as you go.", }, ]; diff --git a/apps/web/test/sidebar.test.tsx b/apps/web/test/sidebar.test.tsx index 73c94f3b3..7927ff57e 100644 --- a/apps/web/test/sidebar.test.tsx +++ b/apps/web/test/sidebar.test.tsx @@ -601,7 +601,9 @@ describe("Sidebar", () => { } expect(container.innerHTML).toContain("No workbenches yet"); expect(container.innerHTML).not.toContain("New Workbench"); - expect(container.querySelector('.shell-ch-row[data-active="true"]')).toBeNull(); + 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"); From 72211c6ce6321d7a30ce1bb8d49a42dcc26e62c0 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 10:13:44 -0700 Subject: [PATCH 3/3] Restore setup-agent check and format templates --- apps/web/src/instant-agent-create.ts | 76 +++++++++++++++++----------- apps/web/src/workbench-templates.ts | 3 +- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/apps/web/src/instant-agent-create.ts b/apps/web/src/instant-agent-create.ts index a9dd2a4ba..cbcd5d34d 100644 --- a/apps/web/src/instant-agent-create.ts +++ b/apps/web/src/instant-agent-create.ts @@ -4,9 +4,8 @@ // picker (`pages/new-workbench-picker.tsx`, CL-6342) and calls // `createWorkbenchFromTemplate` below once a row is chosen. It mints a // fresh `kind: "workbench"` room (CL-6982), not a Myra DM clone -// (`kind: "chat"` + `definitionId`). Rooms do not auto-host Myra: a blank -// mint is an empty room; named templates still instantiate their -// non-Myra participant roster after the room exists. Explicitly defining a +// (`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. @@ -36,6 +35,7 @@ import { } from "./workbench-templates-api"; import { createAgentDefinition, listAgentDefinitions } from "./agents-api"; +import { findMyraDefinition } from "./myra-workbench"; import { workbenchPath } from "./workbench-path"; const log = getLogger("web.instant-agent-create"); @@ -44,17 +44,22 @@ import type { WorkbenchTemplateId } from "./workbench-templates"; export const NEW_WORKBENCH_TITLE = "New room"; /** - * Marks the precondition failure below as intentionally user-facing: its - * `message` is authored copy, never a raw request path or schema summary, - * so a caller can show it verbatim. Every other throw on this path - * (`ApiQueryError`, `ChatApiError`, or a plain `Error` from a package that - * hasn't opted in) must go through that error type's own describer instead - * — allow-listing safe throws, rather than denylisting unsafe ones, so a - * new error type added later fails safe (masked) instead of leaking by - * default. + * Marks the two precondition failures below as intentionally + * user-facing: their `message` is authored copy, never a raw request + * path or schema summary, so a caller can show it verbatim. Every + * other throw on this path (`ApiQueryError`, `ChatApiError`, or a + * plain `Error` from a package that hasn't opted in) must go through + * that error type's own describer instead — allow-listing safe + * throws, rather than denylisting unsafe ones, so a new error type + * added later fails safe (masked) instead of leaking by default. * - * `kind` lets a caller tell "this template genuinely doesn't exist here" - * apart from other failures without parsing `message` text. + * `kind` lets a caller tell "the setup agent isn't deployed yet" apart + * from "this template genuinely doesn't exist here" without parsing + * `message` text: the first is very often a still-provisioning bench + * (CL-6457's background deploy hasn't finished, or never started + * without a credential) that the caller should check + * `fetchAgentReadiness` over before treating as a dead end; the second + * never resolves itself and should surface as-is. */ export class WorkbenchPreconditionError extends Error { readonly kind: "setup-agent-missing" | "template-unavailable"; @@ -67,6 +72,15 @@ export class WorkbenchPreconditionError extends Error { } } +/** + * Consumer-language stand-in for the system precondition this bench + * hit: "no deployed setup agent" describes an internal implementation + * detail, never something a person signing in for the first time + * should have to parse. + */ +const SETUP_AGENT_MISSING_MESSAGE = + "Your workbench is still finishing setup. Try again in a moment."; + /** * Presents the connected org's repo list for the person to pick from once * the workbench exists — the create flow's own "select" half of CL-6386 @@ -83,19 +97,18 @@ export type PickGithubRepos = (args: { /** * 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, and not auto-hosting Myra. - * When the id names a real manifest (`workbenchTemplate`), this also - * creates its non-Myra 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 empty room under the - * generic `NEW_WORKBENCH_TITLE`. When `pickGithubRepos` is supplied and + * `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 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 * CL-6386's "select on new-workbench" step — see `PickGithubRepos`'s - * own doc. When GitHub is still required and not yet connected, the mint - * carries `connectGithubRequiredFor` so the room opens with the - * connect-github card. + * own doc. * * `queryClient` invalidates the workbenches list once every template * participant has been invited (CL-6594) — `ChatWorkspace`'s own @@ -109,8 +122,9 @@ export type PickGithubRepos = (args: { * * `firstMessage`, when given (CL-6628's prompt box), is sent as the * signed-in person's own opening message once the room and its - * template participants exist, so it lands after any template - * greeting rather than racing it. + * template participants exist, so it lands after the setup/template + * greeting rather than racing it — Myra reads the room's actual intent + * as the next line, not the first. */ export async function createWorkbenchFromTemplate( tenantId: string, @@ -120,6 +134,13 @@ export async function createWorkbenchFromTemplate( pickGithubRepos?: PickGithubRepos, firstMessage?: string, ): Promise { + const definitions = await listAgentDefinitions(tenantId); + if (findMyraDefinition(definitions) === undefined) { + throw new WorkbenchPreconditionError( + SETUP_AGENT_MISSING_MESSAGE, + "setup-agent-missing", + ); + } // The manifest comes from the bench library (CL-6344), never from a // hardcoded catalog import; reading it is what seeds the shelf // (CL-6458). `blank` is the one id with no manifest by design; any @@ -157,9 +178,6 @@ export async function createWorkbenchFromTemplate( const workbench = await createWorkbench(tenantId, { kind: "workbench", name: manifest?.title ?? NEW_WORKBENCH_TITLE, - ...(requiresGithub && !githubAlreadyConnected - ? { connectGithubRequiredFor: manifest?.title ?? "" } - : {}), }); if (githubAlreadyConnected && pickGithubRepos !== undefined) { diff --git a/apps/web/src/workbench-templates.ts b/apps/web/src/workbench-templates.ts index 4590320df..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. Bring your own work, connect things as you go.", + promise: "An empty room. Bring your own work, connect things as you go.", }, ];