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
3 changes: 1 addition & 2 deletions apps/web/src/agent-chat-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
// calls this on success so an explicitly-defined new agent never ends
// nowhere, and `instant-agent-create.ts` — THE one creation verb
// (CL-6138) — calls it against the account's default setup template.
// Always creates (CL-6089) — never the `reuseExisting` land-hop path,
// which is `default-agent-workbench.ts`'s own call, not this one.
// Always creates (CL-6089) — never `reuseExisting`.

import { createWorkbench } from "@corbits/chat-ui";

Expand Down
75 changes: 74 additions & 1 deletion apps/web/src/command-palette-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ function context(overrides: {
const ctx = {
path: overrides.path,
navigate: (to: string) => navigated.push(to),
tenantId: overrides.tenantId ?? "tenant-1",
tenantId:
overrides.tenantId === undefined ? "tenant-1" : overrides.tenantId,
cycleTheme: () => {
themeCycled = true;
},
Expand Down Expand Up @@ -152,9 +153,81 @@ describe("runActionCommand", () => {
});

test("talk-to-myra does nothing without a selected bench", async () => {
let fetches = 0;
globalThis.fetch = (() => {
fetches += 1;
throw new Error("null tenant must not fall back to a default tenant");
}) as unknown as typeof fetch;
const { ctx, navigated } = context({ path: "/", tenantId: null });
expect(ctx.tenantId).toBeNull();
await runActionCommand("talk-to-myra", ctx);
expect(navigated).toEqual([]);
expect(fetches).toBe(0);
});

test("talk-to-myra opens the generic agent DM, not a land-hop", async () => {
const posts: { path: string; body: unknown }[] = [];
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
const path =
typeof input === "string" ? input : new URL(String(input)).pathname;
if (path.includes("/workflows/definitions")) {
return Promise.resolve(
new Response(
JSON.stringify({
data: [
{
id: "def-assistant",
tenantId: "tenant-1",
name: "assistant",
currentVersion: "1",
status: "deployed",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
skills: [],
},
],
nextCursor: null,
}),
{ status: 200, headers: { "content-type": "application/json" } },
),
);
}
if (path.endsWith("/chat/workbenches") && init?.method === "POST") {
posts.push({
path,
body: JSON.parse(String(init.body)),
});
return Promise.resolve(
new Response(
JSON.stringify({
id: "chan-dm-myra",
title: "Myra",
kind: "chat",
pinned: false,
participants: [],
}),
{ status: 200, headers: { "content-type": "application/json" } },
),
);
}
throw new Error(`unexpected fetch: ${init?.method ?? "GET"} ${path}`);
}) as unknown as typeof fetch;

const { ctx, navigated } = context({ path: "/" });
await runActionCommand("talk-to-myra", ctx);

expect(posts).toEqual([
{
path: "/api/tenants/tenant-1/chat/workbenches",
body: {
kind: "chat",
definitionId: "def-assistant",
reuseExisting: true,
},
},
]);
expect(navigated).toEqual(["/w/chan-dm-myra"]);
expect(navigated).not.toContain("/");
});
});

Expand Down
13 changes: 8 additions & 5 deletions apps/web/src/command-palette-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
// never hops to `/routines` first the way this used to.

import { createPendingDialogRequest } from "@corbits/shell-layout";
import { WORKBENCH_PATH_PREFIX, workbenchPath } from "./workbench-path";
import { WORKBENCH_PATH_PREFIX } from "./workbench-path";
import { NEW_WORKBENCH_PATH } from "./routes";
import { ensureMyraWorkbench } from "./myra-workbench";
import { listAgentDefinitions } from "./agents-api";
import { openAgentDmChat } from "./agent-dm-launch";
import { findMyraDefinition } from "./myra-workbench";
import { requestLibraryUpload } from "./library-upload";
import type { RoutinePanelSubject } from "./shell/canvas-availability";

Expand Down Expand Up @@ -194,9 +196,10 @@ export async function runActionCommand(
}
case "talk-to-myra": {
if (ctx.tenantId === null) return;
const result = await ensureMyraWorkbench(ctx.tenantId);
if (result.kind === "ready")
ctx.navigate(workbenchPath(result.workbenchId));
const definitions = await listAgentDefinitions(ctx.tenantId);
const myra = findMyraDefinition(definitions);
if (myra === undefined) return;
await openAgentDmChat(ctx.tenantId, myra.id, ctx.navigate);
return;
}
case "go-workbenches": {
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/instant-agent-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
// 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
// seeded `assistant` definition backing Myra, 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
Expand Down
188 changes: 2 additions & 186 deletions apps/web/src/myra-workbench.test.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,8 @@
import { afterEach, describe, expect, test } from "bun:test";
import { describe, expect, test } from "bun:test";

import {
ensureMyraWorkbench,
findMyraWorkbench,
findMyraDefinition,
isMyraWorkbenchId,
isMyraWorkbenchTitle,
MYRA_WORKBENCH_TITLE,
resetMyraWorkbenchCache,
} from "./myra-workbench";
import type { Workbench } from "@corbits/chat-ui";
import { findMyraDefinition } from "./myra-workbench";
import type { AgentDefinition } from "./agents-api";

function workbench(partial: {
readonly id: string;
readonly title: string;
readonly kind?: string;
}): Workbench {
return {
id: partial.id,
title: partial.title,
kind: partial.kind ?? "workbench",
pinned: false,
participants: [],
};
}

describe("myra-workbench helpers", () => {
afterEach(() => {
resetMyraWorkbenchCache();
});

test("isMyraWorkbenchId is false until a workbench id is cached", () => {
expect(isMyraWorkbenchId("chan-1")).toBe(false);
expect(isMyraWorkbenchId(null)).toBe(false);
});

test("MYRA_WORKBENCH_TITLE is Myra", () => {
expect(MYRA_WORKBENCH_TITLE).toBe("Myra");
});

test("isMyraWorkbenchTitle is case-insensitive and trims", () => {
expect(isMyraWorkbenchTitle("Myra")).toBe(true);
expect(isMyraWorkbenchTitle(" myra ")).toBe(true);
expect(isMyraWorkbenchTitle("MYRA")).toBe(true);
expect(isMyraWorkbenchTitle("Myra chat")).toBe(false);
expect(isMyraWorkbenchTitle("Assistant")).toBe(false);
});

test("findMyraWorkbench returns the first Myra-titled row", () => {
const items = [
workbench({ id: "a", title: "general" }),
workbench({ id: "b", title: "myra" }),
workbench({ id: "c", title: "Myra" }),
];
expect(findMyraWorkbench(items)?.id).toBe("b");
});

test("findMyraWorkbench returns undefined when none match", () => {
expect(
findMyraWorkbench([workbench({ id: "a", title: "general" })]),
).toBeUndefined();
});
});

function definition(partial: {
readonly id: string;
readonly name: string;
Expand Down Expand Up @@ -94,126 +33,3 @@ describe("findMyraDefinition", () => {
).toBeUndefined();
});
});

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

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

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" },
});

test("creates a chat with Myra's definitionId when no Myra row exists", async () => {
const calls = stubFetch((path) => {
if (path.endsWith("/chat/workbenches?kind=workbench")) {
return json({ items: [] });
}
if (path.endsWith("/chat/workbenches?kind=chat")) {
return json({ items: [] });
}
if (path.includes("/workflows/definitions")) {
return json({
data: [definition({ id: "def-assistant", name: "assistant" })],
nextCursor: null,
});
}
if (path.endsWith("/chat/workbenches")) {
return json({
id: "chat-1",
title: "Myra",
kind: "chat",
pinned: false,
participants: [],
});
}
throw new Error(`unexpected fetch: ${path}`);
});

const result = await ensureMyraWorkbench("tnt_1");

expect(result).toEqual({ kind: "ready", workbenchId: "chat-1" });
const createCall = calls.find((call) =>
call.path.endsWith("/chat/workbenches"),
);
expect(createCall?.init?.method).toBe("POST");
expect(JSON.parse(String(createCall?.init?.body))).toEqual({
kind: "chat",
definitionId: "def-assistant",
name: "Myra",
reuseExisting: true,
});
expect(isMyraWorkbenchId("chat-1")).toBe(true);
});

test("converts a legacy workbench-kind Myra row carrying the agent into an auto-responding chat", async () => {
const legacyWire = {
id: "legacy-1",
title: "Myra",
kind: "workbench",
pinned: true,
participants: [{ address: "myra@wf_1.tnt_1", handle: "myra" }],
};
const calls = stubFetch((path) => {
if (path.endsWith("/chat/workbenches?kind=workbench")) {
return json({ items: [legacyWire] });
}
if (path.endsWith("/chat/workbenches?kind=chat")) {
return json({ items: [] });
}
if (path.endsWith("/chat/workbenches/legacy-1/settings")) {
return json({
...legacyWire,
kind: "chat",
settings: { "chat/kind": "chat" },
contextWindow: { value: 50, source: "inherit" },
});
}
throw new Error(`unexpected fetch: ${path}`);
});

const result = await ensureMyraWorkbench("tnt_1");

expect(result).toEqual({ kind: "ready", workbenchId: "legacy-1" });
expect(isMyraWorkbenchId("legacy-1")).toBe(true);
const patchCall = calls.find((call) => call.init?.method === "PATCH");
expect(JSON.parse(String(patchCall?.init?.body))).toEqual({
"chat/kind": "chat",
});
});

test("errors when no Myra definition is deployed for the tenant", async () => {
stubFetch((path) => {
if (path.endsWith("/chat/workbenches?kind=workbench"))
return json({ items: [] });
if (path.endsWith("/chat/workbenches?kind=chat"))
return json({ items: [] });
if (path.includes("/workflows/definitions")) {
return json({ data: [], nextCursor: null });
}
throw new Error(`unexpected fetch: ${path}`);
});

const result = await ensureMyraWorkbench("tnt_1");

expect(result.kind).toBe("error");
});
});
Loading
Loading