Skip to content
Merged
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
86 changes: 0 additions & 86 deletions apps/web/src/instant-agent-create.test.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,10 @@
import { afterEach, describe, expect, test } from "bun:test";

import {
createAgentAndLaunch,
createWorkbenchFromTemplate,
NEW_WORKBENCH_TITLE,
} from "./instant-agent-create";

describe("createAgentAndLaunch", () => {
const realFetch = globalThis.fetch;

afterEach(() => {
globalThis.fetch = realFetch;
});

type RecordedCall = { readonly path: string; readonly init?: RequestInit };

function stubFetch(respond: (path: string) => Response): RecordedCall[] {
const calls: RecordedCall[] = [];
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
const path =
typeof input === "string" ? input : new URL(String(input)).pathname;
calls.push(init === undefined ? { path } : { path, init });
return Promise.resolve(respond(path));
}) as typeof fetch;
return calls;
}

const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});

const assistantDefinitionWire = {
id: "def-assistant",
tenantId: "tnt_1",
name: "assistant",
currentVersion: "1",
status: "deployed",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
skills: [] as readonly string[],
};

test("launches a New Workbench chat against the default setup template, no definition drafted", async () => {
const navigated: string[] = [];
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: "chat",
pinned: false,
participants: [],
});
}
throw new Error(`unexpected fetch: ${path}`);
});

await createAgentAndLaunch("tnt_1", (to) => navigated.push(to));

const createCall = calls.find((call) =>
call.path.endsWith("/chat/workbenches"),
);
expect(JSON.parse(String(createCall?.init?.body))).toEqual({
kind: "chat",
definitionId: "def-assistant",
name: NEW_WORKBENCH_TITLE,
});
expect(calls.some((call) => call.path.includes("/agent-definitions"))).toBe(
false,
);
expect(navigated).toEqual(["/w/chan-1"]);
});

test("throws when the tenant has no deployed setup template", async () => {
stubFetch((path) => {
if (path.includes("/workflows/definitions")) {
return json({ data: [], nextCursor: null });
}
throw new Error(`unexpected fetch: ${path}`);
});

await expect(createAgentAndLaunch("tnt_1", () => {})).rejects.toThrow(
"No default setup agent found for this workbench.",
);
});
});

describe("createWorkbenchFromTemplate (CL-6387)", () => {
const realFetch = globalThis.fetch;

Expand Down
59 changes: 17 additions & 42 deletions apps/web/src/instant-agent-create.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
// THE one creation verb (CL-6089, retargeted CL-6138) for the bench's
// zero-workbench land-hop (`home-page.tsx`): it mints a fresh workbench
// titled "New 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.
//
// Every other "create a workbench" affordance — the sidebar's "+", the
// command palette's "New workbench" — opens the template picker
// (`pages/new-workbench-picker.tsx`) instead (CL-6342, superseding
// CL-6138's direct mint for those entry points): `createWorkbenchFromTemplate`
// below is what the picker's "Create workbench" button calls once a row is
// chosen.
// Every "create a workbench" affordance — the sidebar's "+", the command
// palette's "New workbench", 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.

import { getLogger } from "@corbits/client-log";
import {
Expand All @@ -36,7 +32,6 @@ import {
fetchWorkbenchTemplateManifest,
} from "./workbench-templates-api";

import { launchAgentChat } from "./agent-chat-launch";
import { createAgentDefinition, listAgentDefinitions } from "./agents-api";
import { findMyraDefinition } from "./myra-workbench";
import { workbenchPath } from "./workbench-path";
Expand Down Expand Up @@ -71,31 +66,11 @@ export type PickGithubRepos = (args: {
readonly selectedRepoIds: readonly string[];
}) => Promise<readonly string[] | null>;

/**
* Finds the account's default setup template (the seeded `assistant`
* definition) and launches a brand-new "New Workbench" chat against it.
* Throws if the tenant has no deployed setup template — a bench without
* one predates seeding and needs an operator, not a client-side retry.
*/
export async function createAgentAndLaunch(
tenantId: string,
navigate: (to: string) => void,
): Promise<void> {
const definitions = await listAgentDefinitions(tenantId);
const template = findMyraDefinition(definitions);
if (template === undefined) {
throw new WorkbenchPreconditionError(
"No default setup agent found for this workbench.",
);
}
await launchAgentChat(tenantId, template.id, navigate, NEW_WORKBENCH_TITLE);
}

/**
* The template picker's "Create workbench" action (CL-6344): mints a
* fresh "New Workbench" chat against the same default setup template
* `createAgentAndLaunch` uses, passing the picked row's id through as
* `templateId` so the room opens with that template's own intro
* fresh "New Workbench" chat 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
Expand Down
57 changes: 38 additions & 19 deletions apps/web/src/pages/home-page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
// Default land: a brand-new bench with zero workbenches auto-mints its
// first Myra workbench and lands straight in it (CL-6138, superseding the
// CL-6104 describe-screen step) — the same one-creation-verb mint
// `instant-agent-create.ts` gives every other "+ New workbench" control, so
// a fresh bench's very first workbench comes from the exact same path as
// every one after it. A bench that already has one or more lands in (or
// creates) the Myra workbench in the main stage, unchanged. Home as a
// dashboard does not earn its keep — `/` only exists as this hop onto
// `/w/:workbenchId`. Deep links to other pages are unchanged.
// Default land: a bench that already has one or more workbenches lands in
// (or creates) the Myra workbench in the main stage. A brand-new bench with
// zero workbenches has nothing to land in yet, so this hop sends it to the
// guided create surface (`NewWorkbenchPickerRoute`, CL-6342) instead of
// auto-minting an unlabeled "New Workbench" and dropping the person straight
// into it — that auto-mint (CL-6138) is exactly the confusing empty-bench
// landing this hop used to produce. Home as a dashboard does not earn its
// keep — `/` only exists as this hop onto `/w/:workbenchId` or `/new`. Deep
// links to other pages are unchanged.
//
// Right after a provider connect this hop is also the wait (CL-6457's
// deploys run in the background, so landing here can beat them). CL-6462
// settled what that wait looks like: one warm loader and nothing else.
// The land is simply attempted again every few seconds, because launching
// Myra IS the test of whether the person can start — she is deployed
// first (`SETUP_AGENT_ASSET_NAME` leads `DEFAULT_WORKFLOWS`), so the
// moment she answers we go, with every other seeded workflow still
// converging behind us. Readiness is read only to tell a wait from a
// genuine failure, never to draw a progress number: a seed count is an
// implementation detail, and "0 of 5" told a waiting person nothing.
// settled what that wait looks like: one warm loader and nothing else. For
// a zero-workbench bench the wait is for Myra's own definition to exist at
// all — the picker's "Create workbench" needs it too, so checking here
// first means the picker never opens onto a create button that would just
// throw. The check is simply retried every few seconds, because Myra's
// readiness IS the test of whether the person can start — she is deployed
// first (`SETUP_AGENT_ASSET_NAME` leads `DEFAULT_WORKFLOWS`), so the moment
// she's ready we go, with every other seeded workflow still converging
// behind us. Readiness is read only to tell a wait from a genuine failure,
// never to draw a progress number: a seed count is an implementation
// detail, and "0 of 5" told a waiting person nothing.

import { Button, EmptyState, PageShell } from "@corbits/react-ui";
import { Clock, WarningCircle } from "@corbits/icons";
Expand All @@ -29,9 +32,9 @@ import { describeApiError } from "@corbits/api-query";
import { fetchAgentReadiness } from "../onboarding";
import { useBench } from "../bench-context";
import { workbenchPath } from "../workbench-path";
import { createAgentAndLaunch } from "../instant-agent-create";
import { ensureMyraWorkbench } from "../myra-workbench";
import { useNavigate } from "../navigation";
import { NEW_WORKBENCH_PATH } from "../routes";

type LandState =
/** Working on it: the warm loader, whether we are reading the bench's
Expand Down Expand Up @@ -98,11 +101,27 @@ export function HomeRoute({
});
};

// Zero workbenches: wait for Myra's own definition to exist, then send
// the person to the picker rather than minting anything ourselves —
// "she can't start yet" and "here, go create your first workbench"
// are different messages, and only the readiness check tells them
// apart.
const awaitFirstWorkbench = () => {
void fetchAgentReadiness().then((readiness) => {
if (cancelled) return;
if (readiness.kind === "ready" || readiness.kind === "chat-ready") {
navigate(NEW_WORKBENCH_PATH);
return;
}
waitAndRetry();
});
};

void listAllWorkbenches(selectedTenantId).then(
(workbenches) => {
if (cancelled) return;
if (workbenches.length === 0) {
createAgentAndLaunch(selectedTenantId, navigate).catch(classify);
awaitFirstWorkbench();
return;
}
void ensureMyraWorkbench(selectedTenantId).then((result) => {
Expand Down
Loading
Loading