feat(FN-2382): finalize presets-first new-agent tab flow

- Split New Agent step-0 into Presets and Custom tabs with presets-first default behavior
- Refine tab layout and styling in dashboard styles to address review feedback and improve responsive presentation
- Expand NewAgentDialog and AgentsView tests to cover tab switching flows and related route behavior
- Update agent documentation to reflect the revised new-agent dialog flow
This commit is contained in:
Fusion
2026-04-24 14:31:12 -07:00
committed by gsxdsm
parent b7b10a6284
commit 9a84bb1e1a
6 changed files with 432 additions and 208 deletions

View File

@@ -35,8 +35,11 @@ interface RuntimeConfig {
maxTurns: number;
}
type StepZeroTab = "presets" | "custom";
export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAgentDialogProps) {
const [step, setStep] = useState(0);
const [stepZeroTab, setStepZeroTab] = useState<StepZeroTab>("presets");
const [name, setName] = useState("");
const [title, setTitle] = useState("");
const [icon, setIcon] = useState("");
@@ -181,6 +184,7 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
const handleClose = () => {
setStep(0);
setStepZeroTab("presets");
setName("");
setTitle("");
setIcon("");
@@ -265,161 +269,208 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
<div className="agent-dialog-body">
{step === 0 && (
<div>
{/* Quick Start Presets */}
<div className="agent-presets">
<div className="agent-presets-header">
Choose a preset or fill in details manually
</div>
<div className="agent-presets-grid">
{AGENT_PRESETS.map(preset => (
<button
key={preset.id}
type="button"
className={`agent-preset-card${selectedPresetId === preset.id ? " selected" : ""}`}
data-testid={`preset-${preset.id}`}
onClick={() => handlePresetSelect(preset)}
title={preset.title}
>
<span className="agent-preset-icon">{preset.icon}</span>
<span className="agent-preset-name">{preset.name}</span>
<span className="agent-preset-role">{preset.role}</span>
{preset.description && (
<span className="agent-preset-description">{preset.description}</span>
)}
</button>
))}
</div>
<div className="agent-dialog-tabs" role="tablist" aria-label="Agent setup mode">
<button
id="agent-dialog-tab-presets"
type="button"
role="tab"
aria-controls="agent-dialog-panel-presets"
aria-selected={stepZeroTab === "presets"}
tabIndex={stepZeroTab === "presets" ? 0 : -1}
className={`agent-dialog-tab${stepZeroTab === "presets" ? " active" : ""}`}
onClick={() => setStepZeroTab("presets")}
data-testid="agent-dialog-tab-presets"
>
Preset personas
</button>
<button
id="agent-dialog-tab-custom"
type="button"
role="tab"
aria-controls="agent-dialog-panel-custom"
aria-selected={stepZeroTab === "custom"}
tabIndex={stepZeroTab === "custom" ? 0 : -1}
className={`agent-dialog-tab${stepZeroTab === "custom" ? " active" : ""}`}
onClick={() => setStepZeroTab("custom")}
data-testid="agent-dialog-tab-custom"
>
Custom agent
</button>
</div>
<div className="agent-dialog-section">
<div className="agent-dialog-section-header">Identity</div>
<div className="agent-dialog-field">
<label htmlFor="agent-name">Name {!selectedPresetId && <span className="agent-dialog-required">*</span>}</label>
<input
id="agent-name"
type="text"
className="input"
placeholder="e.g. Frontend Reviewer"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-title">Title <span className="agent-dialog-optional">(optional)</span></label>
<input
id="agent-title"
type="text"
className="input"
placeholder="e.g. Senior Code Reviewer"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-icon">Icon <span className="agent-dialog-optional">(optional)</span></label>
<input
id="agent-icon"
type="text"
className="input"
placeholder="e.g. 🤖"
value={icon}
onChange={e => setIcon(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label>Role</label>
<div className="agent-role-grid">
{AGENT_ROLES.map(r => (
<button
key={r.value}
type="button"
className={`agent-role-option${role === r.value ? " selected" : ""}`}
onClick={() => setRole(r.value)}
>
<span className="agent-role-option-icon">{r.icon}</span>
<span className="agent-role-option-label">{r.label}</span>
</button>
))}
{stepZeroTab === "presets" && (
<div
id="agent-dialog-panel-presets"
className="agent-dialog-tab-panel"
role="tabpanel"
aria-labelledby="agent-dialog-tab-presets"
>
<div className="agent-presets">
<div className="agent-presets-header">
Choose a preset persona to prefill role, identity, soul, and instructions
</div>
<div className="agent-presets-grid">
{AGENT_PRESETS.map(preset => (
<button
key={preset.id}
type="button"
className={`agent-preset-card${selectedPresetId === preset.id ? " selected" : ""}`}
data-testid={`preset-${preset.id}`}
onClick={() => handlePresetSelect(preset)}
title={preset.title}
>
<span className="agent-preset-icon">{preset.icon}</span>
<span className="agent-preset-name">{preset.name}</span>
<span className="agent-preset-role">{preset.role}</span>
{preset.description && (
<span className="agent-preset-description">{preset.description}</span>
)}
</button>
))}
</div>
</div>
</div>
</div>
<div className="agent-dialog-section">
<div className="agent-dialog-section-header">Configuration</div>
<div className="agent-dialog-field">
<label htmlFor="agent-reports-to">Reports To <span className="agent-dialog-optional">(optional)</span></label>
<select
id="agent-reports-to"
className="select"
value={reportsTo}
onChange={e => setReportsTo(e.target.value)}
disabled={managersLoading}
>
<option value="">No manager</option>
{availableManagers.map((manager) => (
<option key={manager.id} value={manager.id}>
{manager.name} ({manager.id})
</option>
))}
</select>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-soul">Soul <span className="agent-dialog-optional">(optional)</span></label>
<textarea
id="agent-soul"
className="input"
rows={2}
placeholder="Describe the agent's personality and communication style..."
value={soul}
onChange={e => setSoul(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-memory">Agent Memory <span className="agent-dialog-optional">(optional)</span></label>
<textarea
id="agent-memory"
className="input"
rows={2}
placeholder="Private to this agent — durable preferences, operating habits, and context it should carry across tasks..."
value={memory}
onChange={e => setMemory(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-instructions-path">Instructions Path <span className="agent-dialog-optional">(optional)</span></label>
<input
id="agent-instructions-path"
type="text"
className="input"
placeholder="e.g. .fusion/agents/reviewer.md"
value={instructionsPath}
onChange={e => setInstructionsPath(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-instructions-text">Inline Instructions <span className="agent-dialog-optional">(optional)</span></label>
<textarea
id="agent-instructions-text"
className="input"
rows={4}
placeholder="Add custom behavior instructions..."
value={instructionsText}
onChange={e => setInstructionsText(e.target.value)}
/>
</div>
</div>
{/* AI-assisted generation */}
<div className="agent-dialog-ai-generate">
<button
type="button"
className="btn btn--ai-generate"
onClick={() => setIsGenerationModalOpen(true)}
)}
{stepZeroTab === "custom" && (
<div
id="agent-dialog-panel-custom"
className="agent-dialog-tab-panel"
role="tabpanel"
aria-labelledby="agent-dialog-tab-custom"
>
<span></span>
Generate with AI
</button>
<p className="agent-dialog-ai-hint">
Describe your agent&apos;s role and let AI generate a specification
</p>
</div>
<div className="agent-dialog-section">
<div className="agent-dialog-section-header">Identity</div>
<div className="agent-dialog-field">
<label htmlFor="agent-name">Name {!selectedPresetId && <span className="agent-dialog-required">*</span>}</label>
<input
id="agent-name"
type="text"
className="input"
placeholder="e.g. Frontend Reviewer"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-title">Title <span className="agent-dialog-optional">(optional)</span></label>
<input
id="agent-title"
type="text"
className="input"
placeholder="e.g. Senior Code Reviewer"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-icon">Icon <span className="agent-dialog-optional">(optional)</span></label>
<input
id="agent-icon"
type="text"
className="input"
placeholder="e.g. 🤖"
value={icon}
onChange={e => setIcon(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label>Role</label>
<div className="agent-role-grid">
{AGENT_ROLES.map(r => (
<button
key={r.value}
type="button"
className={`agent-role-option${role === r.value ? " selected" : ""}`}
onClick={() => setRole(r.value)}
>
<span className="agent-role-option-icon">{r.icon}</span>
<span className="agent-role-option-label">{r.label}</span>
</button>
))}
</div>
</div>
</div>
<div className="agent-dialog-section">
<div className="agent-dialog-section-header">Configuration</div>
<div className="agent-dialog-field">
<label htmlFor="agent-reports-to">Reports To <span className="agent-dialog-optional">(optional)</span></label>
<select
id="agent-reports-to"
className="select"
value={reportsTo}
onChange={e => setReportsTo(e.target.value)}
disabled={managersLoading}
>
<option value="">No manager</option>
{availableManagers.map((manager) => (
<option key={manager.id} value={manager.id}>
{manager.name} ({manager.id})
</option>
))}
</select>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-soul">Soul <span className="agent-dialog-optional">(optional)</span></label>
<textarea
id="agent-soul"
className="input"
rows={2}
placeholder="Describe the agent's personality and communication style..."
value={soul}
onChange={e => setSoul(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-memory">Agent Memory <span className="agent-dialog-optional">(optional)</span></label>
<textarea
id="agent-memory"
className="input"
rows={2}
placeholder="Private to this agent — durable preferences, operating habits, and context it should carry across tasks..."
value={memory}
onChange={e => setMemory(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-instructions-path">Instructions Path <span className="agent-dialog-optional">(optional)</span></label>
<input
id="agent-instructions-path"
type="text"
className="input"
placeholder="e.g. .fusion/agents/reviewer.md"
value={instructionsPath}
onChange={e => setInstructionsPath(e.target.value)}
/>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-instructions-text">Inline Instructions <span className="agent-dialog-optional">(optional)</span></label>
<textarea
id="agent-instructions-text"
className="input"
rows={4}
placeholder="Add custom behavior instructions..."
value={instructionsText}
onChange={e => setInstructionsText(e.target.value)}
/>
</div>
</div>
{/* AI-assisted generation */}
<div className="agent-dialog-ai-generate">
<button
type="button"
className="btn btn--ai-generate"
onClick={() => setIsGenerationModalOpen(true)}
>
<span></span>
Generate with AI
</button>
<p className="agent-dialog-ai-hint">
Describe your agent&apos;s role and let AI generate a specification
</p>
</div>
</div>
)}
</div>
)}

View File

@@ -939,7 +939,8 @@ describe("AgentsView", () => {
// Open create dialog
fireEvent.click(screen.getByText("New Agent"));
// Step 0: Fill in agent name
// Step 0: switch to Custom tab and fill in agent name
fireEvent.click(screen.getByRole("tab", { name: "Custom agent" }));
const nameInput = screen.getByPlaceholderText("e.g. Frontend Reviewer");
fireEvent.change(nameInput, { target: { value: "My Agent" } });
@@ -972,10 +973,12 @@ describe("AgentsView", () => {
fireEvent.click(screen.getByText("New Agent"));
// Wait for the dialog to settle after the model fetch completes
// Presets tab is default and custom fields appear after switching tabs
await waitFor(() => {
expect(screen.getByPlaceholderText("e.g. Frontend Reviewer")).toBeTruthy();
expect(screen.getByRole("tab", { name: "Preset personas", selected: true })).toBeTruthy();
});
fireEvent.click(screen.getByRole("tab", { name: "Custom agent" }));
expect(screen.getByPlaceholderText("e.g. Frontend Reviewer")).toBeTruthy();
});
it("does not allow proceeding with empty name", async () => {
@@ -1009,6 +1012,7 @@ describe("AgentsView", () => {
fireEvent.click(screen.getByText("New Agent"));
fireEvent.click(screen.getByRole("tab", { name: "Custom agent" }));
const nameInput = screen.getByPlaceholderText("e.g. Frontend Reviewer");
fireEvent.change(nameInput, { target: { value: "Fail Agent" } });

View File

@@ -134,6 +134,23 @@ function clickModelOption(portal: HTMLElement, optionText: RegExp) {
fireEvent.click(option!);
}
async function openPresetTab(user: ReturnType<typeof userEvent.setup>) {
await user.click(screen.getByRole("tab", { name: "Preset personas" }));
}
async function openCustomTab(user: ReturnType<typeof userEvent.setup>) {
await user.click(screen.getByRole("tab", { name: "Custom agent" }));
}
function openCustomTabSync() {
fireEvent.click(screen.getByRole("tab", { name: "Custom agent" }));
}
function getStepZeroField(label: string | RegExp) {
openCustomTabSync();
return screen.getByLabelText(label);
}
describe("NewAgentDialog", () => {
const mockOnClose = vi.fn();
const mockOnCreated = vi.fn();
@@ -164,17 +181,50 @@ describe("NewAgentDialog", () => {
expect(screen.getByRole("dialog", { name: "Create new agent" })).toBeTruthy();
});
it("shows Identity and Configuration section headers on step 0", async () => {
it("renders tabbed step-0 UI with preset tab active by default", async () => {
await act(async () => {
render(
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
);
});
expect(screen.getByText("Identity")).toBeInTheDocument();
expect(screen.getByText("Configuration")).toBeInTheDocument();
expect(screen.getByLabelText(/Name/)).toBeInTheDocument();
expect(screen.getByLabelText(/Reports To/)).toBeInTheDocument();
const tabList = screen.getByRole("tablist", { name: "Agent setup mode" });
expect(tabList).toBeInTheDocument();
const customTab = screen.getByRole("tab", { name: "Custom agent" });
const presetsTab = screen.getByRole("tab", { name: "Preset personas" });
expect(presetsTab).toHaveAttribute("aria-selected", "true");
expect(presetsTab).toHaveAttribute("aria-controls", "agent-dialog-panel-presets");
expect(customTab).toHaveAttribute("aria-selected", "false");
expect(customTab).toHaveAttribute("aria-controls", "agent-dialog-panel-custom");
expect(screen.getByRole("tabpanel", { name: "Preset personas" })).toBeInTheDocument();
expect(screen.getByTestId("preset-ceo")).toBeInTheDocument();
expect(screen.queryByText("Identity")).toBeNull();
expect(screen.queryByLabelText(/Name/)).toBeNull();
});
it("switches between tabs and preserves manual values", async () => {
const user = userEvent.setup();
render(
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
);
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
await openCustomTab(user);
await user.type(screen.getByLabelText(/Name/), "Custom Value");
await openPresetTab(user);
expect(screen.getByRole("tabpanel", { name: "Preset personas" })).toBeInTheDocument();
expect(screen.getByTestId("preset-ceo")).toBeInTheDocument();
expect(screen.queryByLabelText(/Name/)).toBeNull();
await openCustomTab(user);
const nameInput = getStepZeroField(/Name/) as HTMLInputElement;
expect(nameInput.value).toBe("Custom Value");
});
});
@@ -198,7 +248,7 @@ describe("NewAgentDialog", () => {
expect(mockFetchAgents).toHaveBeenCalledOnce();
});
const reportsToSelect = screen.getByLabelText(/Reports To/) as HTMLSelectElement;
const reportsToSelect = getStepZeroField(/Reports To/) as HTMLSelectElement;
expect(reportsToSelect.tagName).toBe("SELECT");
expect(within(reportsToSelect).getByRole("option", { name: "No manager" })).toBeTruthy();
expect(within(reportsToSelect).getByRole("option", { name: "Manager One (agent-manager-1)" })).toBeTruthy();
@@ -213,10 +263,10 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchAgents).toHaveBeenCalledOnce());
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Agent With Manager");
const reportsToSelect = screen.getByLabelText(/Reports To/);
const reportsToSelect = getStepZeroField(/Reports To/);
await user.selectOptions(reportsToSelect, "agent-manager-1");
await user.click(screen.getByText("Next"));
@@ -245,7 +295,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchAgents).toHaveBeenCalledOnce());
await user.type(screen.getByLabelText(/Name/), "Agent Without Manager");
await user.type(getStepZeroField(/Name/), "Agent Without Manager");
await user.click(screen.getByText("Next"));
await user.click(screen.getByText("Next"));
await user.click(screen.getByText("Create"));
@@ -269,11 +319,11 @@ describe("NewAgentDialog", () => {
expect(mockFetchAgents).toHaveBeenCalledOnce();
});
const reportsToSelect = screen.getByLabelText(/Reports To/) as HTMLSelectElement;
const reportsToSelect = getStepZeroField(/Reports To/) as HTMLSelectElement;
expect(within(reportsToSelect).getByRole("option", { name: "No manager" })).toBeTruthy();
expect(reportsToSelect.options).toHaveLength(1);
await user.type(screen.getByLabelText(/Name/), "Agent Works Without Managers");
await user.type(getStepZeroField(/Name/), "Agent Works Without Managers");
await user.click(screen.getByText("Next"));
await user.click(screen.getByText("Next"));
await user.click(screen.getByText("Create"));
@@ -306,7 +356,7 @@ describe("NewAgentDialog", () => {
);
// Navigate to step 1 (model config step) by filling name and clicking Next
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await fireEvent.change(nameInput, { target: { value: "Test Agent" } });
await fireEvent.click(screen.getByText("Next"));
@@ -332,7 +382,7 @@ describe("NewAgentDialog", () => {
});
// Navigate to step 1
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -353,7 +403,7 @@ describe("NewAgentDialog", () => {
});
// Navigate to step 1
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -372,7 +422,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Navigate to step 1
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -398,7 +448,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Navigate to step 2
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
await user.click(screen.getByText("Next"));
@@ -418,7 +468,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Navigate to step 1
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -445,7 +495,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Step 0: Fill name
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
// Step 1: Navigate and select model
@@ -477,7 +527,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Step 0: Fill name
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
// Step 1: Leave model as default
@@ -506,7 +556,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Step 0: Fill name
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Thinking Agent");
// Step 1: Select model and thinking level
@@ -545,7 +595,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Navigate to step 1 — should still show the dropdown (empty models)
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -564,7 +614,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Fill in name
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Agent Name");
// Navigate to step 1 and select model
@@ -588,7 +638,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledTimes(2));
// Name should be empty
const newNameInput = screen.getByLabelText(/Name/) as HTMLInputElement;
const newNameInput = getStepZeroField(/Name/) as HTMLInputElement;
expect(newNameInput.value).toBe("");
});
});
@@ -600,6 +650,7 @@ describe("NewAgentDialog", () => {
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
);
});
openCustomTabSync();
expect(screen.getByText("Generate with AI")).toBeTruthy();
});
@@ -615,6 +666,7 @@ describe("NewAgentDialog", () => {
expect(screen.queryByTestId("agent-generation-modal")).toBeNull();
// Click the Generate with AI button
openCustomTabSync();
await user.click(screen.getByText("Generate with AI"));
// Generation modal should now be open
@@ -630,6 +682,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Open generation modal and apply spec
openCustomTabSync();
await user.click(screen.getByText("Generate with AI"));
await user.click(screen.getByTestId("generation-modal-apply"));
@@ -656,6 +709,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Open generation modal and apply spec with role "reviewer"
openCustomTabSync();
await user.click(screen.getByText("Generate with AI"));
await user.click(screen.getByTestId("generation-modal-apply"));
@@ -676,6 +730,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Open generation modal and apply spec with unknown role "security-auditor"
openCustomTabSync();
await user.click(screen.getByText("Generate with AI"));
await user.click(screen.getByTestId("generation-modal-apply-custom-role"));
@@ -696,6 +751,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Open generation modal and apply spec
openCustomTabSync();
await user.click(screen.getByText("Generate with AI"));
await user.click(screen.getByTestId("generation-modal-apply"));
@@ -714,10 +770,11 @@ describe("NewAgentDialog", () => {
);
// Fill in a name first
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Manual Name");
// Open generation modal
openCustomTabSync();
await user.click(screen.getByText("Generate with AI"));
expect(screen.getByTestId("agent-generation-modal")).toBeTruthy();
@@ -725,7 +782,7 @@ describe("NewAgentDialog", () => {
await user.click(screen.getByTestId("generation-modal-close"));
// Should still be on step 0 with original name
const nameAfter = screen.getByLabelText(/Name/) as HTMLInputElement;
const nameAfter = getStepZeroField(/Name/) as HTMLInputElement;
expect(nameAfter.value).toBe("Manual Name");
expect(screen.queryByTestId("agent-generation-modal")).toBeNull();
});
@@ -739,6 +796,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Open generation modal and apply spec
openCustomTabSync();
await user.click(screen.getByText("Generate with AI"));
await user.click(screen.getByTestId("generation-modal-apply"));
@@ -763,23 +821,31 @@ describe("NewAgentDialog", () => {
});
describe("preset selection", () => {
it("renders all 20 preset cards in step 0", async () => {
it("renders all 20 preset cards in the preset tab", async () => {
const user = userEvent.setup();
await act(async () => {
render(
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
);
});
await openPresetTab(user);
const presetCards = screen.getAllByTestId(/^preset-/);
expect(presetCards).toHaveLength(20);
});
it("shows the quick start header text", async () => {
it("shows the preset tab header text", async () => {
const user = userEvent.setup();
await act(async () => {
render(
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
);
});
expect(screen.getByText("Choose a preset or fill in details manually")).toBeTruthy();
await openPresetTab(user);
expect(screen.getByText("Choose a preset persona to prefill role, identity, soul, and instructions")).toBeTruthy();
});
it("clicking a preset populates name, title, icon, and role", async () => {
@@ -791,16 +857,18 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Click the "Engineer" preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-engineer"));
// Should advance to step 1 (model config), go back to verify fields
await user.click(screen.getByText("Back"));
await openCustomTab(user);
// Verify form fields were populated
const nameInput = screen.getByLabelText(/Name/) as HTMLInputElement;
const nameInput = getStepZeroField(/Name/) as HTMLInputElement;
expect(nameInput.value).toBe("Engineer");
const titleInput = screen.getByLabelText(/Title/) as HTMLInputElement;
const titleInput = getStepZeroField(/Title/) as HTMLInputElement;
// Title should be set to the preset's description (not the professional title)
expect(titleInput.value).toBe("Implements features, fixes bugs, and writes well-tested code across the full application stack.");
@@ -819,6 +887,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Click the "CTO" preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-cto"));
// Should be on step 1 — model dropdown visible
@@ -834,6 +903,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Click the "CEO" preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-ceo"));
// Go back to step 0 to verify visual feedback
@@ -852,10 +922,12 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Click CEO preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-ceo"));
await user.click(screen.getByText("Back"));
// Click CTO preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-cto"));
await user.click(screen.getByText("Back"));
@@ -864,7 +936,8 @@ describe("NewAgentDialog", () => {
expect(screen.getByTestId("preset-ceo").classList.contains("selected")).toBe(false);
// Name should be updated
const nameInput = screen.getByLabelText(/Name/) as HTMLInputElement;
await openCustomTab(user);
const nameInput = getStepZeroField(/Name/) as HTMLInputElement;
expect(nameInput.value).toBe("CTO");
});
@@ -877,11 +950,13 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Select a preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-engineer"));
await user.click(screen.getByText("Back"));
await openCustomTab(user);
// Override the name manually
const nameInput = screen.getByLabelText(/Name/) as HTMLInputElement;
const nameInput = getStepZeroField(/Name/) as HTMLInputElement;
await user.clear(nameInput);
await user.type(nameInput, "My Custom Engineer");
@@ -897,6 +972,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Select a preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-ceo"));
// Close the dialog
@@ -914,10 +990,11 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledTimes(2));
// Name should be empty (no preset selected)
const nameInput = screen.getByLabelText(/Name/) as HTMLInputElement;
const nameInput = getStepZeroField(/Name/) as HTMLInputElement;
expect(nameInput.value).toBe("");
// No preset cards should be selected
await openPresetTab(user);
const selectedCards = document.querySelectorAll(".agent-preset-card.selected");
expect(selectedCards).toHaveLength(0);
});
@@ -931,6 +1008,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Click the "Reviewer" preset (advances to step 1)
await openPresetTab(user);
await user.click(screen.getByTestId("preset-reviewer"));
// Step 1: navigate to summary
@@ -964,12 +1042,15 @@ describe("NewAgentDialog", () => {
});
it("preset card titles show the professional title", async () => {
const user = userEvent.setup();
await act(async () => {
render(
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
);
});
await openPresetTab(user);
const ceoCard = screen.getByTestId("preset-ceo");
expect(ceoCard.getAttribute("title")).toBe("Chief Executive Officer");
@@ -978,12 +1059,15 @@ describe("NewAgentDialog", () => {
});
it("renders descriptions in all 20 preset cards", async () => {
const user = userEvent.setup();
await act(async () => {
render(
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
);
});
await openPresetTab(user);
// Every preset should have a description element rendered
const descriptionElements = screen.getAllByText(/.\./, {
selector: ".agent-preset-description",
@@ -998,12 +1082,15 @@ describe("NewAgentDialog", () => {
it("all presets have non-empty description strings", async () => {
// Import the array directly by checking the rendered cards
const user = userEvent.setup();
await act(async () => {
render(
<NewAgentDialog isOpen={true} onClose={mockOnClose} onCreated={mockOnCreated} />,
);
});
await openPresetTab(user);
const presetIds = [
"ceo", "cto", "cmo", "cfo", "engineer", "backend-engineer",
"frontend-engineer", "fullstack-engineer", "qa-engineer",
@@ -1029,12 +1116,14 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Click the CEO preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-ceo"));
// Go back to verify the title field
await user.click(screen.getByText("Back"));
await openCustomTab(user);
const titleInput = screen.getByLabelText(/Title/) as HTMLInputElement;
const titleInput = getStepZeroField(/Title/) as HTMLInputElement;
expect(titleInput.value).toBe("Oversees project strategy, sets priorities, and coordinates between departments to ensure alignment with business goals.");
});
@@ -1046,6 +1135,7 @@ describe("NewAgentDialog", () => {
});
// On initial render (step 0, no preset), the * required indicator should be visible
openCustomTabSync();
const nameLabel = screen.getByText("Name", { selector: "label" });
const requiredSpan = nameLabel.querySelector(".agent-dialog-required");
expect(requiredSpan).toBeTruthy();
@@ -1061,10 +1151,12 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Select a preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-engineer"));
// Preset advances to step 1, go back to step 0
await user.click(screen.getByText("Back"));
await openCustomTab(user);
// The * required indicator should NOT be visible when a preset is selected
const nameLabel = screen.getByText("Name", { selector: "label" });
@@ -1081,13 +1173,15 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Select a preset (this fills the name and advances to step 1)
await openPresetTab(user);
await user.click(screen.getByTestId("preset-ceo"));
// Go back to step 0
await user.click(screen.getByText("Back"));
await openCustomTab(user);
// Clear the name field
const nameInput = screen.getByLabelText(/Name/) as HTMLInputElement;
const nameInput = getStepZeroField(/Name/) as HTMLInputElement;
await user.clear(nameInput);
expect(nameInput.value).toBe("");
@@ -1115,6 +1209,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Click the QA Engineer preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-qa-engineer"));
// Navigate to summary and create
@@ -1143,13 +1238,15 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Select a preset (advances to step 1)
await openPresetTab(user);
await user.click(screen.getByTestId("preset-engineer"));
// Go back to step 0 to override instructions
await user.click(screen.getByText("Back"));
await openCustomTab(user);
// Override the instructionsText manually
const instructionsTextarea = screen.getByLabelText(/Inline Instructions/) as HTMLTextAreaElement;
const instructionsTextarea = getStepZeroField(/Inline Instructions/) as HTMLTextAreaElement;
await user.clear(instructionsTextarea);
await user.type(instructionsTextarea, "My custom instructions");
@@ -1178,17 +1275,19 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Click the CTO preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-cto"));
// Go back to step 0 to verify fields
await user.click(screen.getByText("Back"));
await openCustomTab(user);
// Verify soul was populated
const soulTextarea = screen.getByLabelText(/Soul/) as HTMLTextAreaElement;
const soulTextarea = getStepZeroField(/Soul/) as HTMLTextAreaElement;
expect(soulTextarea.value).toContain("pragmatic technologist");
// Verify instructionsText was populated
const instructionsTextarea = screen.getByLabelText(/Inline Instructions/) as HTMLTextAreaElement;
const instructionsTextarea = getStepZeroField(/Inline Instructions/) as HTMLTextAreaElement;
expect(instructionsTextarea.value).toContain("Evaluate technology choices");
});
@@ -1201,23 +1300,27 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Select CEO preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-ceo"));
await user.click(screen.getByText("Back"));
// Verify CEO soul is set
let soulTextarea = screen.getByLabelText(/Soul/) as HTMLTextAreaElement;
await openCustomTab(user);
let soulTextarea = getStepZeroField(/Soul/) as HTMLTextAreaElement;
expect(soulTextarea.value).toContain("strategic leader");
// Select a different preset (DevOps Engineer)
await openPresetTab(user);
await user.click(screen.getByTestId("preset-devops-engineer"));
await user.click(screen.getByText("Back"));
await openCustomTab(user);
// Verify soul updated to DevOps
soulTextarea = screen.getByLabelText(/Soul/) as HTMLTextAreaElement;
soulTextarea = getStepZeroField(/Soul/) as HTMLTextAreaElement;
expect(soulTextarea.value).toContain("infrastructure-minded engineer");
// Verify instructions updated
const instructionsTextarea = screen.getByLabelText(/Inline Instructions/) as HTMLTextAreaElement;
const instructionsTextarea = getStepZeroField(/Inline Instructions/) as HTMLTextAreaElement;
expect(instructionsTextarea.value).toContain("rollback plan");
});
@@ -1230,6 +1333,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Select a preset
await openPresetTab(user);
await user.click(screen.getByTestId("preset-cto"));
// Close the dialog — wait for the state updates to flush before unmounting
@@ -1245,10 +1349,10 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledTimes(2));
// Soul and instructionsText should be empty
const soulTextarea = screen.getByLabelText(/Soul/) as HTMLTextAreaElement;
const soulTextarea = getStepZeroField(/Soul/) as HTMLTextAreaElement;
expect(soulTextarea.value).toBe("");
const instructionsTextarea = screen.getByLabelText(/Inline Instructions/) as HTMLTextAreaElement;
const instructionsTextarea = getStepZeroField(/Inline Instructions/) as HTMLTextAreaElement;
expect(instructionsTextarea.value).toBe("");
});
});
@@ -1268,7 +1372,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
const user = userEvent.setup();
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -1300,7 +1404,7 @@ describe("NewAgentDialog", () => {
});
const user = userEvent.setup();
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -1327,7 +1431,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Navigate to step 1
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -1344,7 +1448,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Navigate to step 1 and add a skill
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -1367,7 +1471,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Navigate to step 1
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
@@ -1396,7 +1500,7 @@ describe("NewAgentDialog", () => {
await waitFor(() => expect(mockFetchModels).toHaveBeenCalledOnce());
// Navigate through steps without adding skills
const nameInput = screen.getByLabelText(/Name/);
const nameInput = getStepZeroField(/Name/);
await user.type(nameInput, "Test Agent");
await user.click(screen.getByText("Next"));
await user.click(screen.getByText("Next"));