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
26 changes: 13 additions & 13 deletions apps/web/src/pages/create-agent-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
// Drafting fails closed, on purpose: if Myra can't draft a prompt
// (unavailable, timed out, an unparseable reply), this panel never
// falls back to a canned template — it surfaces the plain-language
// failure and reveals a manual "System prompt" field in Advanced
// instead, so the person can write their own and try again.
// failure, keeps Advanced collapsed, and the disabled primary names
// the blocker so the person can open Advanced, write their own
// system prompt, and try again.
//
// The identity swatch is preview-only: `@corbits/agent-directory` has
// no field to persist a chosen avatar tone today, so picking one only
Expand Down Expand Up @@ -156,7 +157,9 @@ function advancedFields(
value: model.canonicalName,
label: model.displayName ?? model.canonicalName,
})),
help: "Left unset, Myra picks one — or the workbench default if drafting fails.",
help: draftFailed
? "Left unset, the agent uses the workbench default."
: "Left unset, Myra picks one — or the workbench default if drafting fails.",
},
];
if (!draftFailed) return withModel;
Expand Down Expand Up @@ -208,10 +211,11 @@ const SUGGESTIONS: readonly Suggestion[] = [
},
];

/** The single reason "Get started" is disabled, in plain language — the
* disabled state explains itself rather than leaving a person to guess.
* `null` once nothing blocks submission. Purpose is never a gate — a
* name alone is a supported happy path. */
/** The single reason the primary is disabled, in plain language — the
* disabled label names the blocker rather than leaving "Get started"
* on a button that cannot be used. `null` once nothing blocks
* submission. Purpose is never a gate — a name alone is a supported
* happy path. */
function blockedReason(
values: FormValues,
draftFailed: boolean,
Expand All @@ -221,7 +225,7 @@ function blockedReason(
return "Fix the handle below — lowercase letters, digits, and hyphens only.";
}
if (draftFailed && values.manualSystemPrompt.trim() === "") {
return "Write a system prompt below to continue.";
return "Open Advanced and write a system prompt to continue.";
}
return null;
}
Expand Down Expand Up @@ -366,7 +370,6 @@ export function CreateAgentPanel({
}
} catch (cause) {
setDraftFailed(true);
setAdvancedOpen(true);
setSubmitError(
submitErrorFromCause(
cause,
Expand Down Expand Up @@ -483,15 +486,12 @@ export function CreateAgentPanel({
</label>

<div className="mt-3 flex flex-col gap-2">
{blocked !== null && (
<p className="text-xs text-muted-foreground">{blocked}</p>
)}
<Button
type="button"
onClick={() => void handleSubmit()}
disabled={submitting || blocked !== null}
>
{submitting ? "Creating…" : "Get started"}
{submitting ? "Creating…" : (blocked ?? "Get started")}
</Button>
</div>

Expand Down
47 changes: 38 additions & 9 deletions apps/web/test/create-agent-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
// field), Advanced stays collapsed, submitting drafts a system prompt
// via Myra before deploying, a Suggestions card runs the exact same
// drafting flow with its name+purpose prefilled, and a failed draft
// fails closed — Advanced opens with a manual system prompt field
// rather than falling back to a silent template. Settings' global
// fails closed — the panel surfaces the failure, keeps Advanced
// collapsed, and the disabled primary names the blocker rather than
// silently opening Advanced or falling back to a silent template. Settings' global
// "Agents" directory (this form's original entry point) was cut in
// CL-6121 — agent configuration lives per-workbench now; the chat page's
// "+ New chat" picker moved to instant creation (CL-6081) and no longer
Expand All @@ -27,6 +28,17 @@ function json(body: unknown, status = 200) {
});
}

const CATALOG_MODEL = {
id: "mdl_1",
tenantId: "tenant_1",
canonicalName: "anthropic/claude-sonnet-4",
displayName: "Claude Sonnet 4",
description: null,
disabled: false,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};

const CREATED_DEFINITION = {
id: "wfd_new",
tenantId: "tenant_1",
Expand All @@ -42,6 +54,7 @@ const CREATED_DEFINITION = {
function stubFetch(overrides: {
draft?: (body: Record<string, unknown>) => Response;
create?: (body: Record<string, unknown>) => Response;
models?: readonly unknown[];
}): void {
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
Expand All @@ -50,7 +63,9 @@ function stubFetch(overrides: {
? (JSON.parse(String(init.body)) as Record<string, unknown>)
: {};
if (url.includes("/catalog/models")) {
return Promise.resolve(json({ data: [], nextCursor: null }));
return Promise.resolve(
json({ data: overrides.models ?? [], nextCursor: null }),
);
}
if (url.endsWith("/skills")) {
return Promise.resolve(json({ skills: [] }));
Expand Down Expand Up @@ -166,16 +181,17 @@ describe("CreateAgentPanel happy path", () => {
].find((el) => (el.textContent ?? "").trim() === "New agent");
expect(heading).toBeDefined();
expect(document.body.textContent).not.toContain("New workbench");
fillField("create-agent-name", "Research Buddy");
const cta = findButton("Get started");
expect(cta).toBeDefined();
expect(cta?.textContent).not.toMatch(/workbench/i);
});

test("a name alone is enough — purpose is optional, never a gate", async () => {
await mount();
const button = findButton("Get started");
expect(button?.hasAttribute("disabled")).toBe(true);
expect(document.body.textContent).toContain("Add a name to continue.");
const emptyCta = findButton("Add a name to continue.");
expect(emptyCta?.hasAttribute("disabled")).toBe(true);
expect(findButton("Get started")).toBeUndefined();

fillField("create-agent-name", "Research Buddy");
expect(findButton("Get started")?.hasAttribute("disabled")).toBe(false);
Expand Down Expand Up @@ -293,9 +309,10 @@ describe("CreateAgentPanel Suggestions", () => {
});

describe("CreateAgentPanel drafting failure — fails closed", () => {
test("a failed draft opens Advanced and blocks submission until a manual system prompt is written", async () => {
test("a failed draft keeps Advanced collapsed and names the blocker on the disabled primary", async () => {
let createCalled = false;
stubFetch({
models: [CATALOG_MODEL],
draft: () =>
json(
{
Expand All @@ -314,6 +331,7 @@ describe("CreateAgentPanel drafting failure — fails closed", () => {
});

await mount();
await settle();
fillField("create-agent-name", "Research Buddy");

const button = findButton("Get started");
Expand All @@ -326,13 +344,20 @@ describe("CreateAgentPanel drafting failure — fails closed", () => {
const details = document.querySelector(
".create-agent-advanced",
) as HTMLDetailsElement | null;
expect(details?.open).toBe(true);
expect(details?.open).toBe(false);
expect(document.body.textContent).toContain(
"Myra couldn't draft a starting prompt for that.",
);
expect(document.body.textContent).toContain("Reference: ref_1");
expect(findButton("Get started")).toBeUndefined();
const blockedCta = findButton(
"Open Advanced and write a system prompt to continue.",
);
expect(blockedCta).toBeDefined();
expect(blockedCta?.hasAttribute("disabled")).toBe(true);
expect(document.body.textContent).not.toContain("Myra picks one");
expect(document.body.textContent).toContain(
"Write a system prompt below to continue.",
"Left unset, the agent uses the workbench default.",
);

const manualField = document.getElementById(
Expand Down Expand Up @@ -381,6 +406,10 @@ describe("CreateAgentPanel drafting failure — fails closed", () => {
expect(document.body.textContent).toContain("Reference: ref_sink_1");
expect(document.body.textContent).not.toContain("The server answered 500.");
expect(document.body.textContent).not.toContain("at draft");
const details = document.querySelector(
".create-agent-advanced",
) as HTMLDetailsElement | null;
expect(details?.open).toBe(false);
});
});

Expand Down
Loading