diff --git a/apps/web/src/pages/create-agent-panel.tsx b/apps/web/src/pages/create-agent-panel.tsx
index 00db734c8..afdd0c362 100644
--- a/apps/web/src/pages/create-agent-panel.tsx
+++ b/apps/web/src/pages/create-agent-panel.tsx
@@ -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
@@ -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;
@@ -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,
@@ -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;
}
@@ -366,7 +370,6 @@ export function CreateAgentPanel({
}
} catch (cause) {
setDraftFailed(true);
- setAdvancedOpen(true);
setSubmitError(
submitErrorFromCause(
cause,
@@ -483,15 +486,12 @@ export function CreateAgentPanel({
- {blocked !== null && (
-
{blocked}
- )}
diff --git a/apps/web/test/create-agent-panel.test.tsx b/apps/web/test/create-agent-panel.test.tsx
index cd4e1c0ec..fbee17094 100644
--- a/apps/web/test/create-agent-panel.test.tsx
+++ b/apps/web/test/create-agent-panel.test.tsx
@@ -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
@@ -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",
@@ -42,6 +54,7 @@ const CREATED_DEFINITION = {
function stubFetch(overrides: {
draft?: (body: Record) => Response;
create?: (body: Record) => Response;
+ models?: readonly unknown[];
}): void {
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
@@ -50,7 +63,9 @@ function stubFetch(overrides: {
? (JSON.parse(String(init.body)) as Record)
: {};
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: [] }));
@@ -166,6 +181,7 @@ 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);
@@ -173,9 +189,9 @@ describe("CreateAgentPanel happy path", () => {
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);
@@ -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(
{
@@ -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");
@@ -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(
@@ -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);
});
});