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 ad46484126
commit db6bf20db4
6 changed files with 432 additions and 208 deletions

View File

@@ -181,7 +181,12 @@ There are two ways to provide custom instructions:
## New Agent Presets (Dashboard UI) ## New Agent Presets (Dashboard UI)
The New Agent dialog in the dashboard provides quick-start presets for common agent roles. Each preset includes: The New Agent dialog keeps the existing 3-step flow, and step 0 is split into two tabs:
- **Preset personas** (default) — quick-start persona cards that prefill the same fields and immediately advance to step 1 when selected
- **Custom agent** — manual setup for identity, configuration, and the Generate with AI entry point
The dashboard provides quick-start presets for common agent roles. Each preset includes:
- **Name and icon** - Display identification - **Name and icon** - Display identification
- **Professional title** - Descriptive role title - **Professional title** - Descriptive role title

View File

@@ -35,8 +35,11 @@ interface RuntimeConfig {
maxTurns: number; maxTurns: number;
} }
type StepZeroTab = "presets" | "custom";
export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAgentDialogProps) { export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAgentDialogProps) {
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [stepZeroTab, setStepZeroTab] = useState<StepZeroTab>("presets");
const [name, setName] = useState(""); const [name, setName] = useState("");
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [icon, setIcon] = useState(""); const [icon, setIcon] = useState("");
@@ -181,6 +184,7 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
const handleClose = () => { const handleClose = () => {
setStep(0); setStep(0);
setStepZeroTab("presets");
setName(""); setName("");
setTitle(""); setTitle("");
setIcon(""); setIcon("");
@@ -265,161 +269,208 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
<div className="agent-dialog-body"> <div className="agent-dialog-body">
{step === 0 && ( {step === 0 && (
<div> <div>
{/* Quick Start Presets */} <div className="agent-dialog-tabs" role="tablist" aria-label="Agent setup mode">
<div className="agent-presets"> <button
<div className="agent-presets-header"> id="agent-dialog-tab-presets"
Choose a preset or fill in details manually type="button"
</div> role="tab"
<div className="agent-presets-grid"> aria-controls="agent-dialog-panel-presets"
{AGENT_PRESETS.map(preset => ( aria-selected={stepZeroTab === "presets"}
<button tabIndex={stepZeroTab === "presets" ? 0 : -1}
key={preset.id} className={`agent-dialog-tab${stepZeroTab === "presets" ? " active" : ""}`}
type="button" onClick={() => setStepZeroTab("presets")}
className={`agent-preset-card${selectedPresetId === preset.id ? " selected" : ""}`} data-testid="agent-dialog-tab-presets"
data-testid={`preset-${preset.id}`} >
onClick={() => handlePresetSelect(preset)} Preset personas
title={preset.title} </button>
> <button
<span className="agent-preset-icon">{preset.icon}</span> id="agent-dialog-tab-custom"
<span className="agent-preset-name">{preset.name}</span> type="button"
<span className="agent-preset-role">{preset.role}</span> role="tab"
{preset.description && ( aria-controls="agent-dialog-panel-custom"
<span className="agent-preset-description">{preset.description}</span> aria-selected={stepZeroTab === "custom"}
)} tabIndex={stepZeroTab === "custom" ? 0 : -1}
</button> className={`agent-dialog-tab${stepZeroTab === "custom" ? " active" : ""}`}
))} onClick={() => setStepZeroTab("custom")}
</div> data-testid="agent-dialog-tab-custom"
>
Custom agent
</button>
</div> </div>
<div className="agent-dialog-section">
<div className="agent-dialog-section-header">Identity</div> {stepZeroTab === "presets" && (
<div className="agent-dialog-field"> <div
<label htmlFor="agent-name">Name {!selectedPresetId && <span className="agent-dialog-required">*</span>}</label> id="agent-dialog-panel-presets"
<input className="agent-dialog-tab-panel"
id="agent-name" role="tabpanel"
type="text" aria-labelledby="agent-dialog-tab-presets"
className="input" >
placeholder="e.g. Frontend Reviewer" <div className="agent-presets">
value={name} <div className="agent-presets-header">
onChange={e => setName(e.target.value)} Choose a preset persona to prefill role, identity, soul, and instructions
/> </div>
</div> <div className="agent-presets-grid">
<div className="agent-dialog-field"> {AGENT_PRESETS.map(preset => (
<label htmlFor="agent-title">Title <span className="agent-dialog-optional">(optional)</span></label> <button
<input key={preset.id}
id="agent-title" type="button"
type="text" className={`agent-preset-card${selectedPresetId === preset.id ? " selected" : ""}`}
className="input" data-testid={`preset-${preset.id}`}
placeholder="e.g. Senior Code Reviewer" onClick={() => handlePresetSelect(preset)}
value={title} title={preset.title}
onChange={e => setTitle(e.target.value)} >
/> <span className="agent-preset-icon">{preset.icon}</span>
</div> <span className="agent-preset-name">{preset.name}</span>
<div className="agent-dialog-field"> <span className="agent-preset-role">{preset.role}</span>
<label htmlFor="agent-icon">Icon <span className="agent-dialog-optional">(optional)</span></label> {preset.description && (
<input <span className="agent-preset-description">{preset.description}</span>
id="agent-icon" )}
type="text" </button>
className="input" ))}
placeholder="e.g. 🤖" </div>
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>
</div> )}
<div className="agent-dialog-section">
<div className="agent-dialog-section-header">Configuration</div> {stepZeroTab === "custom" && (
<div className="agent-dialog-field"> <div
<label htmlFor="agent-reports-to">Reports To <span className="agent-dialog-optional">(optional)</span></label> id="agent-dialog-panel-custom"
<select className="agent-dialog-tab-panel"
id="agent-reports-to" role="tabpanel"
className="select" aria-labelledby="agent-dialog-tab-custom"
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> <div className="agent-dialog-section">
Generate with AI <div className="agent-dialog-section-header">Identity</div>
</button> <div className="agent-dialog-field">
<p className="agent-dialog-ai-hint"> <label htmlFor="agent-name">Name {!selectedPresetId && <span className="agent-dialog-required">*</span>}</label>
Describe your agent&apos;s role and let AI generate a specification <input
</p> id="agent-name"
</div> 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> </div>
)} )}

View File

@@ -939,7 +939,8 @@ describe("AgentsView", () => {
// Open create dialog // Open create dialog
fireEvent.click(screen.getByText("New Agent")); 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"); const nameInput = screen.getByPlaceholderText("e.g. Frontend Reviewer");
fireEvent.change(nameInput, { target: { value: "My Agent" } }); fireEvent.change(nameInput, { target: { value: "My Agent" } });
@@ -972,10 +973,12 @@ describe("AgentsView", () => {
fireEvent.click(screen.getByText("New Agent")); 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(() => { 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 () => { it("does not allow proceeding with empty name", async () => {
@@ -1009,6 +1012,7 @@ describe("AgentsView", () => {
fireEvent.click(screen.getByText("New Agent")); fireEvent.click(screen.getByText("New Agent"));
fireEvent.click(screen.getByRole("tab", { name: "Custom agent" }));
const nameInput = screen.getByPlaceholderText("e.g. Frontend Reviewer"); const nameInput = screen.getByPlaceholderText("e.g. Frontend Reviewer");
fireEvent.change(nameInput, { target: { value: "Fail Agent" } }); fireEvent.change(nameInput, { target: { value: "Fail Agent" } });

View File

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

View File

@@ -29537,9 +29537,52 @@ html .column.drag-over * {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.agent-dialog-tabs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-sm);
margin-bottom: var(--space-lg);
}
.agent-dialog-tab {
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--card);
color: var(--text-muted);
padding: var(--space-sm) var(--space-md);
font-size: calc(var(--space-sm) + var(--space-xs));
font-weight: 600;
cursor: pointer;
transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast);
}
.agent-dialog-tab:hover {
border-color: var(--todo);
color: var(--text);
background: var(--card-hover);
}
.agent-dialog-tab:focus-visible {
outline: none;
border-color: var(--todo);
box-shadow: var(--focus-ring-strong);
}
.agent-dialog-tab.active {
border-color: var(--todo);
color: var(--text);
background: color-mix(in srgb, var(--todo) 12%, transparent);
}
.agent-dialog-tab-panel {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.agent-role-grid { .agent-role-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(calc(var(--space-xl) * 3 + var(--space-sm)), 1fr));
gap: var(--space-sm); gap: var(--space-sm);
} }
@@ -29684,10 +29727,10 @@ html .column.drag-over * {
.agent-presets-grid { .agent-presets-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(calc(var(--space-xl) * 5 + var(--space-lg) + var(--space-xs)), 1fr));
gap: var(--space-sm); gap: var(--space-sm);
margin-bottom: var(--space-lg); margin-bottom: var(--space-lg);
max-height: 400px; max-height: calc(var(--space-xl) * 16 + var(--space-lg));
overflow-y: auto; overflow-y: auto;
} }
@@ -29710,6 +29753,12 @@ html .column.drag-over * {
background: var(--bg-tertiary); background: var(--bg-tertiary);
} }
.agent-preset-card:focus-visible {
outline: none;
border-color: var(--todo);
box-shadow: var(--focus-ring-strong);
}
.agent-preset-card.selected { .agent-preset-card.selected {
border-color: var(--todo); border-color: var(--todo);
background: color-mix(in srgb, var(--todo) 12%, transparent); background: color-mix(in srgb, var(--todo) 12%, transparent);
@@ -34710,6 +34759,17 @@ html .column.drag-over * {
height: calc(var(--space-xs) - var(--space-xs) * 0.25); height: calc(var(--space-xs) - var(--space-xs) * 0.25);
} }
.agent-dialog-tabs {
grid-template-columns: 1fr;
gap: calc(var(--space-sm) - var(--space-xs) * 0.5);
margin-bottom: var(--space-md);
}
.agent-dialog-tab {
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
text-align: left;
}
/* Agent Import specific */ /* Agent Import specific */
.agent-import-file-upload { .agent-import-file-upload {
flex-direction: column; flex-direction: column;

View File

@@ -771,7 +771,7 @@ describe("POST /api/projects route handler", () => {
expect((res.body as { error?: string }).error).toContain("Git clone failed"); expect((res.body as { error?: string }).error).toContain("Git clone failed");
expect(mockRegisterProject).not.toHaveBeenCalled(); expect(mockRegisterProject).not.toHaveBeenCalled();
expect(mockFsRm).toHaveBeenCalledWith("/tmp/broken-clone", { recursive: true, force: true }); expect(mockFsRm).toHaveBeenCalledWith("/tmp/broken-clone", { recursive: true, force: true });
}); }, 15_000);
it("rejects clone mode when destination directory is non-empty", async () => { it("rejects clone mode when destination directory is non-empty", async () => {
const store = new MockStoreForRoutes(); const store = new MockStoreForRoutes();