Files
fusion/packages/dashboard/app/components/NewAgentDialog.tsx
gsxdsm 925c66308f feat(FN-1345): add soul and instructionsText to agent presets
- Add soul and instructionsText fields to all 20 built-in agent presets
- Enable soul-based agent visualization and instructions preview in NewAgentDialog
- Add comprehensive tests for preset soul and instructionsText rendering flow
- Include changeset for @gsxdsm/fusion minor release
2026-04-09 08:47:54 -07:00

738 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from "react";
import type { AgentCapability, ModelInfo, AgentGenerationSpec } from "../api";
import { createAgent, fetchModels } from "../api";
import { CustomModelDropdown } from "./CustomModelDropdown";
import { ProviderIcon } from "./ProviderIcon";
import { AgentGenerationModal } from "./AgentGenerationModal";
export interface NewAgentDialogProps {
isOpen: boolean;
onClose: () => void;
onCreated: () => void;
projectId?: string;
}
const AGENT_ROLES: { value: AgentCapability; label: string; icon: string }[] = [
{ value: "triage", label: "Triage", icon: "🔍" },
{ value: "executor", label: "Executor", icon: "⚡" },
{ value: "reviewer", label: "Reviewer", icon: "👁" },
{ value: "merger", label: "Merger", icon: "🔀" },
{ value: "scheduler", label: "Scheduler", icon: "⏰" },
{ value: "engineer", label: "Engineer", icon: "🛠" },
{ value: "custom", label: "Custom", icon: "🔧" },
];
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high";
/** Set of valid AgentCapability values for mapping generated roles */
const VALID_CAPABILITIES = new Set<string>(["triage", "executor", "reviewer", "merger", "scheduler", "engineer", "custom"]);
interface RuntimeConfig {
model: string;
thinkingLevel: ThinkingLevel;
maxTurns: number;
}
/** Preset agent template for one-click creation */
interface AgentPreset {
/** Unique identifier for the preset */
id: string;
/** Display name (e.g., "CEO", "CTO") */
name: string;
/** Emoji icon */
icon: string;
/** Professional title (e.g., "Chief Executive Officer") */
title: string;
/** Agent capability role */
role: AgentCapability;
/** Optional description of the agent's responsibilities */
description?: string;
/** Personality/identity description for the agent (1-3 sentences, first person) */
soul?: string;
/** Custom behavioral instructions for the agent (3-8 actionable lines) */
instructionsText?: string;
}
const AGENT_PRESETS: AgentPreset[] = [
{
id: "ceo",
name: "CEO",
icon: "👔",
title: "Chief Executive Officer",
role: "custom",
description: "Oversees project strategy, sets priorities, and coordinates between departments to ensure alignment with business goals.",
soul: "I am a strategic leader who sees the big picture and connects every decision to business outcomes. I communicate with clarity and conviction, balancing ambition with pragmatism.",
instructionsText: "Always evaluate proposals against strategic goals before approving.\nPrioritize work that delivers the highest business value.\nCommunicate decisions with clear rationale and context.\nIdentify cross-team dependencies and resolve blockers proactively.\nKeep long-term vision in focus while making short-term tradeoffs.",
},
{
id: "cto",
name: "CTO",
icon: "🧠",
title: "Chief Technology Officer",
role: "custom",
description: "Defines technical architecture, evaluates technology choices, and guides engineering standards across the project.",
soul: "I am a pragmatic technologist who balances innovation with stability. I think in systems, weigh tradeoffs carefully, and communicate technical concepts in terms of risk and impact.",
instructionsText: "Evaluate technology choices against scalability, maintainability, and team expertise.\nIdentify architectural risks early and propose mitigations.\nReview changes for adherence to established patterns and conventions.\nPrioritize simplicity — reject over-engineering and unnecessary abstraction.\nDocument architectural decisions with rationale for future reference.",
},
{
id: "cmo",
name: "CMO",
icon: "📢",
title: "Chief Marketing Officer",
role: "custom",
description: "Drives product positioning, audience engagement strategy, and content planning to grow user adoption.",
soul: "I am a growth-focused strategist who understands audiences deeply. I craft messaging that resonates, identify channels that convert, and measure everything to optimize impact.",
instructionsText: "Evaluate all user-facing content for clarity and audience alignment.\nPropose messaging that highlights user benefits over technical features.\nIdentify growth opportunities from product changes and new features.\nEnsure consistent brand voice across all touchpoints.\nTrack engagement metrics to validate content and campaign effectiveness.",
},
{
id: "cfo",
name: "CFO",
icon: "💰",
title: "Chief Financial Officer",
role: "custom",
description: "Manages budget allocation, cost optimization, and financial planning to maximize resource efficiency.",
soul: "I am a disciplined financial steward who quantifies everything. I challenge assumptions with data, flag cost risks early, and ensure every investment has a clear return.",
instructionsText: "Evaluate proposals for cost implications before implementation.\nFlag infrastructure or dependency changes that may increase operating costs.\nQuantify the impact of technical debt in terms of developer time and risk.\nRecommend cost optimization opportunities in existing systems.\nEnsure resource allocation aligns with project priorities.",
},
{
id: "engineer",
name: "Engineer",
icon: "👨‍💻",
title: "Software Engineer",
role: "engineer",
description: "Implements features, fixes bugs, and writes well-tested code across the full application stack.",
soul: "I am a reliable and versatile engineer who writes clean, well-tested code. I value clarity over cleverness and always leave the codebase better than I found it.",
instructionsText: "Write tests for every new function and bug fix.\nFollow existing code patterns and conventions in the project.\nKeep functions small, focused, and well-named.\nHandle error cases explicitly — never silently swallow errors.\nDocument complex logic with inline comments explaining the why, not the what.",
},
{
id: "backend-engineer",
name: "Backend Engineer",
icon: "⚙️",
title: "Backend Engineer",
role: "engineer",
description: "Builds and maintains server-side logic, APIs, database schemas, and background processing pipelines.",
soul: "I am a methodical backend engineer who prioritizes data integrity, API reliability, and system performance. I think carefully about edge cases and failure modes.",
instructionsText: "Validate and sanitize all inputs at API boundaries.\nUse transactions for multi-step database operations.\nAdd appropriate indexes for new query patterns.\nHandle concurrent access with proper locking or optimistic concurrency.\nLog meaningful context for debugging without exposing sensitive data.\nDesign APIs with consistent error responses and status codes.",
},
{
id: "frontend-engineer",
name: "Frontend Engineer",
icon: "🎨",
title: "Frontend Engineer",
role: "engineer",
description: "Develops user interfaces, manages component libraries, and ensures responsive, accessible UI experiences.",
soul: "I am a detail-oriented frontend engineer who cares deeply about user experience. I build interfaces that are accessible, responsive, and performant by default.",
instructionsText: "Ensure all interactive elements are keyboard-accessible with proper ARIA labels.\nTest responsive behavior at common breakpoints.\nKeep bundle size small — avoid importing entire libraries for single functions.\nUse semantic HTML elements over divs where appropriate.\nHandle loading, error, and empty states for every data-driven component.\nFollow the project's existing component patterns and naming conventions.",
},
{
id: "fullstack-engineer",
name: "Fullstack Engineer",
icon: "🚀",
title: "Full Stack Engineer",
role: "engineer",
description: "Works across frontend and backend to deliver end-to-end features from database to user interface.",
soul: "I am an end-to-end builder who connects database to UI seamlessly. I understand the full request lifecycle and optimize across layers rather than in isolation.",
instructionsText: "Consider the full data flow from database schema through API to UI component.\nKeep backend and frontend changes cohesive within a single feature.\nEnsure API contracts match frontend expectations before implementing.\nWrite integration-level tests that validate cross-layer behavior.\nOptimize at the right layer — don't compensate for backend issues in frontend code.",
},
{
id: "qa-engineer",
name: "QA Engineer",
icon: "🧪",
title: "Quality Assurance Engineer",
role: "engineer",
description: "Designs test plans, writes automated tests, and validates that features meet acceptance criteria before release.",
soul: "I am a thorough and methodical QA engineer who never assumes code works — I verify. I communicate issues clearly with reproduction steps and severity assessments.",
instructionsText: "Always run the full test suite before approving any changes.\nWrite regression tests for every bug fix.\nCheck boundary conditions and edge cases for every new feature.\nValidate error handling and input sanitization.\nReport issues with clear reproduction steps and expected vs actual behavior.",
},
{
id: "devops-engineer",
name: "DevOps Engineer",
icon: "🔧",
title: "DevOps Engineer",
role: "engineer",
description: "Manages infrastructure, deployment pipelines, and monitoring to ensure reliable and scalable service delivery.",
soul: "I am an infrastructure-minded engineer who automates everything and distrusts manual processes. I design for failure and always have a rollback plan.",
instructionsText: "Never deploy on Fridays or before weekends without a rollback plan.\nEnsure all infrastructure changes are version-controlled and reproducible.\nAdd health checks and monitoring for new services and endpoints.\nValidate deployment scripts in a staging environment before production.\nDocument runbooks for common operational incidents.",
},
{
id: "ci-engineer",
name: "CI Engineer",
icon: "⚡",
title: "CI/CD Engineer",
role: "engineer",
description: "Builds and optimizes continuous integration and delivery pipelines for fast, reliable release cycles.",
soul: "I am a pipeline optimization specialist who hates slow builds and flaky tests. I make the development feedback loop as fast and reliable as possible.",
instructionsText: "Fail fast — order pipeline stages from quickest to slowest.\nCache dependencies aggressively to reduce build times.\nIsolate tests that depend on external services or shared state.\nAdd pipeline status badges and failure notifications.\nMeasure and report pipeline duration trends over time.",
},
{
id: "security-engineer",
name: "Security Engineer",
icon: "🛡️",
title: "Security Engineer",
role: "engineer",
description: "Identifies vulnerabilities, enforces security best practices, and conducts audits to protect application integrity.",
soul: "I am a security-first engineer who assumes nothing is trustworthy by default. I think like an attacker to find weaknesses before they become incidents.",
instructionsText: "Never hardcode secrets, API keys, or credentials in source code.\nValidate and sanitize all user inputs at every trust boundary.\nUse parameterized queries — never concatenate user input into SQL or shell commands.\nCheck dependencies for known vulnerabilities before introducing them.\nApply the principle of least privilege to all access control decisions.",
},
{
id: "data-engineer",
name: "Data Engineer",
icon: "📊",
title: "Data Engineer",
role: "engineer",
description: "Designs data pipelines, manages storage infrastructure, and ensures reliable data flow for analytics and features.",
soul: "I am a data reliability specialist who knows that bad data silently corrupts everything downstream. I validate early, transform carefully, and monitor constantly.",
instructionsText: "Validate data at ingestion points — never trust upstream sources blindly.\nDesign idempotent pipeline steps that handle reprocessing gracefully.\nAdd data quality checks and anomaly detection at key pipeline stages.\nDocument data schemas and breaking change procedures clearly.\nEnsure pipeline failures trigger alerts with actionable error context.",
},
{
id: "ml-engineer",
name: "ML Engineer",
icon: "🤖",
title: "Machine Learning Engineer",
role: "engineer",
description: "Builds, trains, and deploys machine learning models, and integrates AI capabilities into the product.",
soul: "I am an ML practitioner who values reproducible experiments and production-grade deployments. I track every experiment, version every model, and measure real-world impact.",
instructionsText: "Version datasets and models alongside code changes.\nLog training parameters, metrics, and results for every experiment.\nImplement fallback behavior when model inference fails or times out.\nMonitor model predictions for drift and degradation in production.\nWrite unit tests for data preprocessing and feature engineering pipelines.",
},
{
id: "product-manager",
name: "Product Manager",
icon: "📋",
title: "Product Manager",
role: "custom",
description: "Defines product requirements, prioritizes the backlog, and coordinates cross-functional delivery from concept to launch.",
soul: "I am a user-obsessed product manager who writes crisp requirements and challenges scope creep. I prioritize ruthlessly and communicate progress transparently.",
instructionsText: "Write requirements with clear acceptance criteria and user stories.\nValidate feature requests against user needs and business goals.\nBreak large initiatives into small, shippable increments.\nFlag scope creep early and propose minimum viable alternatives.\nEnsure every feature has a measurable success metric.",
},
{
id: "designer",
name: "Designer",
icon: "✏️",
title: "Product Designer",
role: "custom",
description: "Creates wireframes, prototypes, and design systems that balance usability, aesthetics, and brand consistency.",
soul: "I am a user-centered designer who believes great design is invisible. I solve problems with clarity and consistency, and I advocate for users who can't advocate for themselves.",
instructionsText: "Evaluate UI changes against existing design system tokens and patterns.\nEnsure sufficient color contrast and readable typography in all themes.\nProvide clear visual hierarchy — users should know what to do next.\nDesign for keyboard and screen reader users, not just mouse users.\nKeep component variants minimal — add new variants only when existing ones don't fit.",
},
{
id: "marketing-manager",
name: "Marketing Manager",
icon: "📣",
title: "Marketing Manager",
role: "custom",
description: "Plans campaigns, manages content channels, and analyzes market data to drive brand awareness and growth.",
soul: "I am a data-driven marketer who tests assumptions and scales what works. I write copy that converts, plan campaigns that compound, and measure relentlessly.",
instructionsText: "Write user-facing copy that is clear, concise, and action-oriented.\nEnsure marketing claims are accurate and verifiable by the product.\nSegment messaging for different audience personas and channels.\nInclude clear calls-to-action in all content.\nTrack and report campaign performance with actionable insights.",
},
{
id: "technical-writer",
name: "Technical Writer",
icon: "📝",
title: "Technical Writer",
role: "custom",
description: "Writes and maintains documentation, API references, and guides that help users and developers succeed.",
soul: "I am a clarity-focused writer who makes complex topics accessible. I write for the reader, not for myself, and I verify every instruction by following it myself.",
instructionsText: "Write procedural docs with numbered steps and expected outcomes.\nInclude code examples that are complete, runnable, and tested.\nUse consistent terminology — define terms on first use and stick to them.\nUpdate documentation when code behavior changes — treat docs as code.\nStructure content with clear headings for easy scanning and navigation.",
},
{
id: "triage",
name: "Triage Agent",
icon: "🔍",
title: "Task Triage Agent",
role: "triage",
description: "Analyzes incoming tasks, generates detailed specifications, and prepares PROMPT.md files for execution.",
soul: "I am a precise and thorough triage agent who transforms vague ideas into actionable specifications. I ask clarifying questions when requirements are ambiguous and never assume context.",
instructionsText: "Generate detailed PROMPT.md files with clear steps and acceptance criteria.\nIdentify missing information and flag ambiguities before specification.\nBreak complex tasks into well-defined, sequenced implementation steps.\nSpecify file scope and dependencies for each task.\nInclude relevant context files for the executor to read first.",
},
{
id: "reviewer",
name: "Reviewer",
icon: "👁️",
title: "Code Reviewer",
role: "reviewer",
description: "Reviews code changes for correctness, security, performance, and adherence to project coding standards.",
soul: "I am a rigorous and fair code reviewer who catches issues without being pedantic. I focus on what matters — correctness, security, and maintainability — and let style debates go.",
instructionsText: "Check for security vulnerabilities in every change — injection, auth bypass, data exposure.\nVerify error handling is explicit and informative, not silent failures.\nEnsure new code follows existing patterns and conventions in the codebase.\nLook for missing tests on new logic paths, especially edge cases.\nFlag performance concerns only when they have measurable impact.",
},
];
export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAgentDialogProps) {
const [step, setStep] = useState(0);
const [name, setName] = useState("");
const [title, setTitle] = useState("");
const [icon, setIcon] = useState("");
const [role, setRole] = useState<AgentCapability>("custom");
const [reportsTo, setReportsTo] = useState("");
const [instructionsPath, setInstructionsPath] = useState("");
const [instructionsText, setInstructionsText] = useState("");
const [soul, setSoul] = useState("");
const [runtimeConfig, setRuntimeConfig] = useState<RuntimeConfig>({
model: "",
thinkingLevel: "off",
maxTurns: 10,
});
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isGenerationModalOpen, setIsGenerationModalOpen] = useState(false);
// Model dropdown state
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
const [modelsLoading, setModelsLoading] = useState(false);
const [favoriteProviders, setFavoriteProviders] = useState<string[]>([]);
const [favoriteModels, setFavoriteModels] = useState<string[]>([]);
// Load models on mount (global data, not per-agent)
useEffect(() => {
setModelsLoading(true);
fetchModels()
.then((response) => {
setAvailableModels(response.models);
setFavoriteProviders(response.favoriteProviders);
setFavoriteModels(response.favoriteModels);
})
.catch(() => {
// Gracefully handle — dropdown will show empty list
})
.finally(() => setModelsLoading(false));
}, []);
// Selected model in "provider/modelId" format, or "" for default
const selectedModel = runtimeConfig.model.includes("/")
? runtimeConfig.model
: "";
const handleGenerated = useCallback((spec: AgentGenerationSpec) => {
// Map generated role to AgentCapability, default to "custom" if unrecognized
const mappedRole = VALID_CAPABILITIES.has(spec.role)
? (spec.role as AgentCapability)
: "custom";
setName(spec.title);
setTitle(spec.description);
setIcon(spec.icon);
setRole(mappedRole);
setRuntimeConfig(c => ({
...c,
thinkingLevel: spec.thinkingLevel,
maxTurns: spec.maxTurns,
}));
setIsGenerationModalOpen(false);
// Advance to Step 1 so user can review model selection
setStep(1);
}, []);
const handleModelChange = useCallback((value: string) => {
// value is "provider/modelId" or "" for default
setRuntimeConfig(c => ({ ...c, model: value }));
}, []);
const handleToggleFavorite = useCallback(async (provider: string) => {
const currentFavorites = favoriteProviders;
const isFavorite = currentFavorites.includes(provider);
const newFavorites = isFavorite
? currentFavorites.filter(p => p !== provider)
: [provider, ...currentFavorites];
setFavoriteProviders(newFavorites);
}, [favoriteProviders]);
const handleToggleModelFavorite = useCallback(async (modelId: string) => {
const currentFavorites = favoriteModels;
const isFavorite = currentFavorites.includes(modelId);
const newFavorites = isFavorite
? currentFavorites.filter(m => m !== modelId)
: [modelId, ...currentFavorites];
setFavoriteModels(newFavorites);
}, [favoriteModels]);
const handlePresetSelect = useCallback((preset: AgentPreset) => {
setSelectedPresetId(preset.id);
setName(preset.name);
setIcon(preset.icon);
setTitle(preset.description ?? preset.title);
setRole(preset.role);
setSoul(preset.soul ?? "");
setInstructionsText(preset.instructionsText ?? "");
// Advance to Step 1 so user can review model selection
setStep(1);
}, []);
if (!isOpen) return null;
const handleClose = () => {
setStep(0);
setName("");
setTitle("");
setIcon("");
setRole("custom");
setReportsTo("");
setInstructionsPath("");
setInstructionsText("");
setSoul("");
setRuntimeConfig({ model: "", thinkingLevel: "off", maxTurns: 10 });
setSelectedPresetId(null);
setError(null);
setIsGenerationModalOpen(false);
onClose();
};
const handleCreate = async () => {
if (!name.trim()) return;
setIsSubmitting(true);
setError(null);
try {
const runtimeCfg: Record<string, unknown> = {};
if (runtimeConfig.model.trim()) runtimeCfg.model = runtimeConfig.model.trim();
if (runtimeConfig.thinkingLevel !== "off") runtimeCfg.thinkingLevel = runtimeConfig.thinkingLevel;
if (runtimeConfig.maxTurns !== 10) runtimeCfg.maxTurns = runtimeConfig.maxTurns;
await createAgent({
name: name.trim(),
role,
...(title.trim() ? { title: title.trim() } : {}),
...(icon.trim() ? { icon: icon.trim() } : {}),
...(reportsTo.trim() ? { reportsTo: reportsTo.trim() } : {}),
...(instructionsPath.trim() ? { instructionsPath: instructionsPath.trim() } : {}),
...(instructionsText.trim() ? { instructionsText: instructionsText.trim() } : {}),
...(soul.trim() ? { soul: soul.trim() } : {}),
...(Object.keys(runtimeCfg).length > 0 ? { runtimeConfig: runtimeCfg } : {}),
}, projectId);
handleClose();
onCreated();
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to create agent");
} finally {
setIsSubmitting(false);
}
};
const selectedRole = AGENT_ROLES.find(r => r.value === role);
return (
<div className="agent-dialog-overlay" onClick={(e) => { if (e.target === e.currentTarget) handleClose(); }}>
<div className="agent-dialog" role="dialog" aria-modal="true" aria-label="Create new agent">
{/* Header */}
<div className="agent-dialog-header">
<span className="agent-dialog-header-title">New Agent</span>
<button
className="btn-icon"
onClick={handleClose}
aria-label="Close"
>
×
</button>
</div>
{/* Step indicator */}
<div className="agent-dialog-steps">
{[0, 1, 2].map(i => (
<div
key={i}
className={`agent-dialog-step${i === step ? " active" : i < step ? " completed" : ""}`}
aria-label={`Step ${i + 1}`}
/>
))}
</div>
{/* Body */}
<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>
<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>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 className="agent-dialog-field">
<label htmlFor="agent-reports-to">Reports To <span className="agent-dialog-optional">(optional agent ID)</span></label>
<input
id="agent-reports-to"
type="text"
className="input"
placeholder="e.g. agent-1234abcd"
value={reportsTo}
onChange={e => setReportsTo(e.target.value)}
/>
</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-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>
{/* 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>
)}
{step === 1 && (
<div>
<div className="agent-dialog-field">
<label>Model</label>
{modelsLoading ? (
<div className="agent-dialog-loading">Loading models</div>
) : (
<CustomModelDropdown
id="agent-model"
label="Model"
value={selectedModel}
onChange={handleModelChange}
models={availableModels}
placeholder="Select a model…"
favoriteProviders={favoriteProviders}
onToggleFavorite={handleToggleFavorite}
favoriteModels={favoriteModels}
onToggleModelFavorite={handleToggleModelFavorite}
/>
)}
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-thinking">Thinking Level</label>
<select
id="agent-thinking"
className="select"
value={runtimeConfig.thinkingLevel}
onChange={e => setRuntimeConfig(c => ({ ...c, thinkingLevel: e.target.value as ThinkingLevel }))}
>
<option value="off">Off</option>
<option value="minimal">Minimal</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div className="agent-dialog-field">
<label htmlFor="agent-max-turns">Max Turns</label>
<input
id="agent-max-turns"
type="number"
className="input"
min={1}
max={500}
value={runtimeConfig.maxTurns}
onChange={e => setRuntimeConfig(c => ({ ...c, maxTurns: Math.max(1, parseInt(e.target.value, 10) || 1) }))}
/>
</div>
</div>
)}
{step === 2 && (
<div>
<p className="agent-dialog-info">
Review your agent configuration before creating.
</p>
<div className="agent-dialog-summary">
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Name</span>
<span className="agent-dialog-summary-row-value">
{icon && <span className="agent-dialog-icon-prefix">{icon}</span>}
{name}
</span>
</div>
{title && (
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Title</span>
<span>{title}</span>
</div>
)}
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Role</span>
<span>{selectedRole?.icon} {selectedRole?.label}</span>
</div>
{reportsTo.trim() && (
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Reports To</span>
<span>{reportsTo.trim()}</span>
</div>
)}
{instructionsPath.trim() && (
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Instructions File</span>
<span>{instructionsPath.trim()}</span>
</div>
)}
{instructionsText.trim() && (
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Inline Instructions</span>
<span>{instructionsText.trim().length} chars</span>
</div>
)}
{soul.trim() && (
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Soul</span>
<span>{soul.trim().length} chars</span>
</div>
)}
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Model</span>
<span>
{selectedModel ? (
<>
<ProviderIcon provider={selectedModel.split("/")[0]} size="sm" />
{" "}
{(() => {
const slashIdx = selectedModel.indexOf("/");
const provider = selectedModel.slice(0, slashIdx);
const modelId = selectedModel.slice(slashIdx + 1);
const model = availableModels.find(m => m.provider === provider && m.id === modelId);
return model?.name || selectedModel;
})()}
</>
) : (
<em className="agent-dialog-summary-row-value--muted">default</em>
)}
</span>
</div>
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Thinking</span>
<span className="agent-dialog-summary-row-value--capitalize">{runtimeConfig.thinkingLevel}</span>
</div>
<div className="agent-dialog-summary-row">
<span className="agent-dialog-summary-row-label">Max Turns</span>
<span>{runtimeConfig.maxTurns}</span>
</div>
</div>
{error && (
<p className="agent-dialog-error">{error}</p>
)}
</div>
)}
</div>
{/* Footer */}
<div className="agent-dialog-footer">
{step > 0 && (
<button className="btn" onClick={() => setStep(s => s - 1)} disabled={isSubmitting}>
Back
</button>
)}
<button className="btn" onClick={handleClose} disabled={isSubmitting}>
Cancel
</button>
{step < 2 ? (
<button
className="btn btn--primary"
onClick={() => setStep(s => s + 1)}
disabled={step === 0 && !name.trim() && !selectedPresetId}
>
Next
</button>
) : (
<button
className="btn btn--primary"
onClick={() => void handleCreate()}
disabled={isSubmitting || !name.trim()}
>
{isSubmitting ? "Creating..." : "Create"}
</button>
)}
</div>
</div>
{/* AI-assisted agent generation modal */}
<AgentGenerationModal
isOpen={isGenerationModalOpen}
onClose={() => setIsGenerationModalOpen(false)}
onGenerated={handleGenerated}
projectId={projectId}
/>
</div>
);
}