Skip to content

Commit 0938a70

Browse files
Keep Advanced collapsed after a draft fail and name the CTA blocker (#611)
Fixes CL-6751 Stacked on CL-6749. A failed Myra draft already shows a consumer sentence and refId; this change stops that failure from silently opening Advanced, puts the blocker on the disabled primary instead of "Get started", and stops model help from claiming Myra will pick one after drafting already failed. - Draft failure keeps Advanced collapsed - Disabled primary names the blocker (`Open Advanced and write a system prompt to continue.`) - Model help after a failed draft: workbench default, not "Myra picks one" - CL-6749 envelope/refId rendering is unchanged
1 parent 26c191c commit 0938a70

2 files changed

Lines changed: 51 additions & 22 deletions

File tree

‎apps/web/src/pages/create-agent-panel.tsx‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@
2727
// Drafting fails closed, on purpose: if Myra can't draft a prompt
2828
// (unavailable, timed out, an unparseable reply), this panel never
2929
// falls back to a canned template — it surfaces the plain-language
30-
// failure and reveals a manual "System prompt" field in Advanced
31-
// instead, so the person can write their own and try again.
30+
// failure, keeps Advanced collapsed, and the disabled primary names
31+
// the blocker so the person can open Advanced, write their own
32+
// system prompt, and try again.
3233
//
3334
// The identity swatch is preview-only: `@corbits/agent-directory` has
3435
// no field to persist a chosen avatar tone today, so picking one only
@@ -156,7 +157,9 @@ function advancedFields(
156157
value: model.canonicalName,
157158
label: model.displayName ?? model.canonicalName,
158159
})),
159-
help: "Left unset, Myra picks one — or the workbench default if drafting fails.",
160+
help: draftFailed
161+
? "Left unset, the agent uses the workbench default."
162+
: "Left unset, Myra picks one — or the workbench default if drafting fails.",
160163
},
161164
];
162165
if (!draftFailed) return withModel;
@@ -208,10 +211,11 @@ const SUGGESTIONS: readonly Suggestion[] = [
208211
},
209212
];
210213

211-
/** The single reason "Get started" is disabled, in plain language — the
212-
* disabled state explains itself rather than leaving a person to guess.
213-
* `null` once nothing blocks submission. Purpose is never a gate — a
214-
* name alone is a supported happy path. */
214+
/** The single reason the primary is disabled, in plain language — the
215+
* disabled label names the blocker rather than leaving "Get started"
216+
* on a button that cannot be used. `null` once nothing blocks
217+
* submission. Purpose is never a gate — a name alone is a supported
218+
* happy path. */
215219
function blockedReason(
216220
values: FormValues,
217221
draftFailed: boolean,
@@ -221,7 +225,7 @@ function blockedReason(
221225
return "Fix the handle below — lowercase letters, digits, and hyphens only.";
222226
}
223227
if (draftFailed && values.manualSystemPrompt.trim() === "") {
224-
return "Write a system prompt below to continue.";
228+
return "Open Advanced and write a system prompt to continue.";
225229
}
226230
return null;
227231
}
@@ -366,7 +370,6 @@ export function CreateAgentPanel({
366370
}
367371
} catch (cause) {
368372
setDraftFailed(true);
369-
setAdvancedOpen(true);
370373
setSubmitError(
371374
submitErrorFromCause(
372375
cause,
@@ -483,15 +486,12 @@ export function CreateAgentPanel({
483486
</label>
484487

485488
<div className="mt-3 flex flex-col gap-2">
486-
{blocked !== null && (
487-
<p className="text-xs text-muted-foreground">{blocked}</p>
488-
)}
489489
<Button
490490
type="button"
491491
onClick={() => void handleSubmit()}
492492
disabled={submitting || blocked !== null}
493493
>
494-
{submitting ? "Creating…" : "Get started"}
494+
{submitting ? "Creating…" : (blocked ?? "Get started")}
495495
</Button>
496496
</div>
497497

‎apps/web/test/create-agent-panel.test.tsx‎

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

31+
const CATALOG_MODEL = {
32+
id: "mdl_1",
33+
tenantId: "tenant_1",
34+
canonicalName: "anthropic/claude-sonnet-4",
35+
displayName: "Claude Sonnet 4",
36+
description: null,
37+
disabled: false,
38+
createdAt: "2026-01-01T00:00:00.000Z",
39+
updatedAt: "2026-01-01T00:00:00.000Z",
40+
};
41+
3042
const CREATED_DEFINITION = {
3143
id: "wfd_new",
3244
tenantId: "tenant_1",
@@ -42,6 +54,7 @@ const CREATED_DEFINITION = {
4254
function stubFetch(overrides: {
4355
draft?: (body: Record<string, unknown>) => Response;
4456
create?: (body: Record<string, unknown>) => Response;
57+
models?: readonly unknown[];
4558
}): void {
4659
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
4760
const url = String(input);
@@ -50,7 +63,9 @@ function stubFetch(overrides: {
5063
? (JSON.parse(String(init.body)) as Record<string, unknown>)
5164
: {};
5265
if (url.includes("/catalog/models")) {
53-
return Promise.resolve(json({ data: [], nextCursor: null }));
66+
return Promise.resolve(
67+
json({ data: overrides.models ?? [], nextCursor: null }),
68+
);
5469
}
5570
if (url.endsWith("/skills")) {
5671
return Promise.resolve(json({ skills: [] }));
@@ -166,16 +181,17 @@ describe("CreateAgentPanel happy path", () => {
166181
].find((el) => (el.textContent ?? "").trim() === "New agent");
167182
expect(heading).toBeDefined();
168183
expect(document.body.textContent).not.toContain("New workbench");
184+
fillField("create-agent-name", "Research Buddy");
169185
const cta = findButton("Get started");
170186
expect(cta).toBeDefined();
171187
expect(cta?.textContent).not.toMatch(/workbench/i);
172188
});
173189

174190
test("a name alone is enough — purpose is optional, never a gate", async () => {
175191
await mount();
176-
const button = findButton("Get started");
177-
expect(button?.hasAttribute("disabled")).toBe(true);
178-
expect(document.body.textContent).toContain("Add a name to continue.");
192+
const emptyCta = findButton("Add a name to continue.");
193+
expect(emptyCta?.hasAttribute("disabled")).toBe(true);
194+
expect(findButton("Get started")).toBeUndefined();
179195

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

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

316333
await mount();
334+
await settle();
317335
fillField("create-agent-name", "Research Buddy");
318336

319337
const button = findButton("Get started");
@@ -326,13 +344,20 @@ describe("CreateAgentPanel drafting failure — fails closed", () => {
326344
const details = document.querySelector(
327345
".create-agent-advanced",
328346
) as HTMLDetailsElement | null;
329-
expect(details?.open).toBe(true);
347+
expect(details?.open).toBe(false);
330348
expect(document.body.textContent).toContain(
331349
"Myra couldn't draft a starting prompt for that.",
332350
);
333351
expect(document.body.textContent).toContain("Reference: ref_1");
352+
expect(findButton("Get started")).toBeUndefined();
353+
const blockedCta = findButton(
354+
"Open Advanced and write a system prompt to continue.",
355+
);
356+
expect(blockedCta).toBeDefined();
357+
expect(blockedCta?.hasAttribute("disabled")).toBe(true);
358+
expect(document.body.textContent).not.toContain("Myra picks one");
334359
expect(document.body.textContent).toContain(
335-
"Write a system prompt below to continue.",
360+
"Left unset, the agent uses the workbench default.",
336361
);
337362

338363
const manualField = document.getElementById(
@@ -381,6 +406,10 @@ describe("CreateAgentPanel drafting failure — fails closed", () => {
381406
expect(document.body.textContent).toContain("Reference: ref_sink_1");
382407
expect(document.body.textContent).not.toContain("The server answered 500.");
383408
expect(document.body.textContent).not.toContain("at draft");
409+
const details = document.querySelector(
410+
".create-agent-advanced",
411+
) as HTMLDetailsElement | null;
412+
expect(details?.open).toBe(false);
384413
});
385414
});
386415

0 commit comments

Comments
 (0)