Skip to content
Closed
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
6 changes: 4 additions & 2 deletions apps/web/src/command-palette-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/command-palette-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 41 additions & 0 deletions apps/web/src/instant-agent-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 18 additions & 30 deletions apps/web/src/instant-agent-create.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -98,18 +95,15 @@ export type PickGithubRepos = (args: {
}) => Promise<readonly string[] | null>;

/**
* 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
Expand Down Expand Up @@ -141,8 +135,7 @@ export async function createWorkbenchFromTemplate(
firstMessage?: string,
): Promise<void> {
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",
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/new-workbench-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export function NewWorkbenchPickerRoute() {
return (
<div className="flex h-full min-h-0 flex-col">
<StageTopBar
crumbs={[{ label: "New workbench" }]}
crumbs={[{ label: "New room" }]}
actions={
<Button
type="button"
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export const SETTINGS_PATH = "/settings";
* view. */
export const MISSION_CONTROL_PATH = "/mission-control";

/** The template picker (CL-6342) — every "+ New workbench" affordance
/** The template picker (CL-6342) — every "+ New room" affordance
* (sidebar, command palette) hops here first; picking a row is what
* actually mints the workbench. Not in `NAV_ROUTES`: it has no sidebar
* row of its own, only the "+" control and the palette reach it. */
Expand Down Expand Up @@ -253,7 +253,7 @@ function InboxRedirect({
export const APP_ROUTES: readonly AppRoute[] = [
{
path: "/",
label: "New Workbench",
label: "New room",
icon: <ChatCircle />,
render: () => <HomeRoute />,
hasStageTopBar: false,
Expand All @@ -268,7 +268,7 @@ export const APP_ROUTES: readonly AppRoute[] = [
},
{
path: NEW_WORKBENCH_PATH,
label: "New workbench",
label: "New room",
icon: <ChatCircle />,
render: () => <NewWorkbenchPickerRoute />,
},
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/shell/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ export function Sidebar({
<Button
variant="ghost"
size="sm"
aria-label="New workbench"
title="New workbench"
aria-label="New room"
title="New room"
onClick={() => onNavigate(NEW_WORKBENCH_PATH)}
>
<Plus />
Expand Down
39 changes: 5 additions & 34 deletions apps/web/src/shell/workbench-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,36 +56,6 @@ function NeedsYouSignal({ tenantId }: { readonly tenantId: string | null }) {
);
}

/**
* The always-active, no-op row naming the current screen as "where the
* person already is" (CL-6124) while a brand-new bench's zero-workbench
* `/` land is auto-minting its first Myra workbench (CL-6138) — a
* transient state, but one that still deserves an honest sidebar row
* rather than an empty list. Styled as the already-active row per house
* rule (grey structure, orange edge for "here"); selecting it is a no-op
* since it names the current screen, not a destination.
*/
function NewWorkbenchStubRow() {
return (
<button
type="button"
className="shell-ch-row"
data-active="true"
aria-current="true"
>
<span className="shell-ch-stack" aria-hidden="true">
<span>N</span>
</span>
<span className="shell-ch-meta">
<span className="shell-ch-name-row">
<span className="shell-ch-name">New Workbench</span>
</span>
</span>
<span className="shell-ch-right" />
</button>
);
}

/**
* The ellipsis-menu item labels for a workbench row: rename and the
* pin/unpin toggle — a strict subset of the conversation's own settings.
Expand Down Expand Up @@ -404,10 +374,11 @@ export function WorkbenchList({
return (
<div className="panel-stack" aria-label="Workbenches">
<NeedsYouSignal tenantId={selectedTenantId} />
<h2 className="shell-panel-list-label">Workbenches</h2>
<div className="panel-stack-group">
<NewWorkbenchStubRow />
</div>
<EmptyState
icon={<Hash />}
title="No workbenches yet"
description="Start a new one with the + above."
/>
</div>
);
}
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/workbench-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
];
17 changes: 11 additions & 6 deletions apps/web/test/new-workbench-picker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/test/routes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
24 changes: 12 additions & 12 deletions apps/web/test/sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"');
Expand Down Expand Up @@ -197,7 +197,7 @@ describe("Sidebar", () => {
});

const newButton = container.querySelector<HTMLButtonElement>(
'[aria-label="New workbench"]',
'[aria-label="New room"]',
);
await act(async () => {
newButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
Expand Down Expand Up @@ -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);
Expand All @@ -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();
});
Expand Down
12 changes: 11 additions & 1 deletion packages/chat-ui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading