mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
feat: add dedicated testing agents and enhanced parallel orchestration
Introduce a new testing agent architecture that runs regression tests independently from coding agents, improving quality assurance in parallel mode. Key changes: Testing Agent System: - Add testing_prompt.template.md for dedicated testing agent role - Add feature_mark_failing MCP tool for regression detection - Add --agent-type flag to select initializer/coding/testing mode - Remove regression testing from coding prompt (now handled by testing agents) Parallel Orchestrator Enhancements: - Add testing agent spawning with configurable ratio (--testing-agent-ratio) - Add comprehensive debug logging system (DebugLog class) - Improve database session management to prevent stale reads - Add engine.dispose() calls to refresh connections after subprocess commits - Fix f-string linting issues (remove unnecessary f-prefixes) UI Improvements: - Add testing agent mascot (Chip) to AgentAvatar - Enhance AgentCard to display testing agent status - Add testing agent ratio slider in SettingsModal - Update WebSocket handling for testing agent updates - Improve ActivityFeed to show testing agent activity API & Server Updates: - Add testing_agent_ratio to settings schema and endpoints - Update process manager to support testing agent type - Enhance WebSocket messages for agent_update events Template Changes: - Delete coding_prompt_yolo.template.md (consolidated into main prompt) - Update initializer_prompt.template.md with improved structure - Streamline coding_prompt.template.md workflow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,11 +83,30 @@ export function ActivityFeed({ activities, maxItems = 5, showHeader = true }: Ac
|
||||
|
||||
function getMascotColor(name: AgentMascot): string {
|
||||
const colors: Record<AgentMascot, string> = {
|
||||
// Original 5
|
||||
Spark: '#3B82F6',
|
||||
Fizz: '#F97316',
|
||||
Octo: '#8B5CF6',
|
||||
Hoot: '#22C55E',
|
||||
Buzz: '#EAB308',
|
||||
// Tech-inspired
|
||||
Pixel: '#EC4899',
|
||||
Byte: '#06B6D4',
|
||||
Nova: '#F43F5E',
|
||||
Chip: '#84CC16',
|
||||
Bolt: '#FBBF24',
|
||||
// Energetic
|
||||
Dash: '#14B8A6',
|
||||
Zap: '#A855F7',
|
||||
Gizmo: '#64748B',
|
||||
Turbo: '#EF4444',
|
||||
Blip: '#10B981',
|
||||
// Playful
|
||||
Neon: '#D946EF',
|
||||
Widget: '#6366F1',
|
||||
Zippy: '#F59E0B',
|
||||
Quirk: '#0EA5E9',
|
||||
Flux: '#7C3AED',
|
||||
}
|
||||
return colors[name] || '#6B7280'
|
||||
}
|
||||
|
||||
@@ -8,11 +8,30 @@ interface AgentAvatarProps {
|
||||
}
|
||||
|
||||
const AVATAR_COLORS: Record<AgentMascot, { primary: string; secondary: string; accent: string }> = {
|
||||
// Original 5
|
||||
Spark: { primary: '#3B82F6', secondary: '#60A5FA', accent: '#DBEAFE' }, // Blue robot
|
||||
Fizz: { primary: '#F97316', secondary: '#FB923C', accent: '#FFEDD5' }, // Orange fox
|
||||
Octo: { primary: '#8B5CF6', secondary: '#A78BFA', accent: '#EDE9FE' }, // Purple octopus
|
||||
Hoot: { primary: '#22C55E', secondary: '#4ADE80', accent: '#DCFCE7' }, // Green owl
|
||||
Buzz: { primary: '#EAB308', secondary: '#FACC15', accent: '#FEF9C3' }, // Yellow bee
|
||||
// Tech-inspired
|
||||
Pixel: { primary: '#EC4899', secondary: '#F472B6', accent: '#FCE7F3' }, // Pink
|
||||
Byte: { primary: '#06B6D4', secondary: '#22D3EE', accent: '#CFFAFE' }, // Cyan
|
||||
Nova: { primary: '#F43F5E', secondary: '#FB7185', accent: '#FFE4E6' }, // Rose
|
||||
Chip: { primary: '#84CC16', secondary: '#A3E635', accent: '#ECFCCB' }, // Lime
|
||||
Bolt: { primary: '#FBBF24', secondary: '#FCD34D', accent: '#FEF3C7' }, // Amber
|
||||
// Energetic
|
||||
Dash: { primary: '#14B8A6', secondary: '#2DD4BF', accent: '#CCFBF1' }, // Teal
|
||||
Zap: { primary: '#A855F7', secondary: '#C084FC', accent: '#F3E8FF' }, // Violet
|
||||
Gizmo: { primary: '#64748B', secondary: '#94A3B8', accent: '#F1F5F9' }, // Slate
|
||||
Turbo: { primary: '#EF4444', secondary: '#F87171', accent: '#FEE2E2' }, // Red
|
||||
Blip: { primary: '#10B981', secondary: '#34D399', accent: '#D1FAE5' }, // Emerald
|
||||
// Playful
|
||||
Neon: { primary: '#D946EF', secondary: '#E879F9', accent: '#FAE8FF' }, // Fuchsia
|
||||
Widget: { primary: '#6366F1', secondary: '#818CF8', accent: '#E0E7FF' }, // Indigo
|
||||
Zippy: { primary: '#F59E0B', secondary: '#FBBF24', accent: '#FEF3C7' }, // Orange-yellow
|
||||
Quirk: { primary: '#0EA5E9', secondary: '#38BDF8', accent: '#E0F2FE' }, // Sky
|
||||
Flux: { primary: '#7C3AED', secondary: '#8B5CF6', accent: '#EDE9FE' }, // Purple
|
||||
}
|
||||
|
||||
const SIZES = {
|
||||
@@ -150,12 +169,335 @@ function BuzzSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Buzz; size: nu
|
||||
)
|
||||
}
|
||||
|
||||
// Pixel - cute pixel art style character
|
||||
function PixelSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Pixel; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Blocky body */}
|
||||
<rect x="20" y="28" width="24" height="28" fill={colors.primary} />
|
||||
<rect x="16" y="32" width="8" height="20" fill={colors.secondary} />
|
||||
<rect x="40" y="32" width="8" height="20" fill={colors.secondary} />
|
||||
{/* Head */}
|
||||
<rect x="16" y="8" width="32" height="24" fill={colors.primary} />
|
||||
{/* Eyes */}
|
||||
<rect x="20" y="14" width="8" height="8" fill="white" />
|
||||
<rect x="36" y="14" width="8" height="8" fill="white" />
|
||||
<rect x="24" y="16" width="4" height="4" fill="#1a1a1a" />
|
||||
<rect x="38" y="16" width="4" height="4" fill="#1a1a1a" />
|
||||
{/* Mouth */}
|
||||
<rect x="26" y="26" width="12" height="4" fill={colors.accent} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Byte - data cube character
|
||||
function ByteSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Byte; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* 3D cube body */}
|
||||
<polygon points="32,8 56,20 56,44 32,56 8,44 8,20" fill={colors.primary} />
|
||||
<polygon points="32,8 56,20 32,32 8,20" fill={colors.secondary} />
|
||||
<polygon points="32,32 56,20 56,44 32,56" fill={colors.accent} opacity="0.6" />
|
||||
{/* Face */}
|
||||
<circle cx="24" cy="28" r="4" fill="white" />
|
||||
<circle cx="40" cy="28" r="4" fill="white" />
|
||||
<circle cx="25" cy="29" r="2" fill="#1a1a1a" />
|
||||
<circle cx="41" cy="29" r="2" fill="#1a1a1a" />
|
||||
<path d="M26,38 Q32,42 38,38" stroke="white" strokeWidth="2" fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Nova - star character
|
||||
function NovaSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Nova; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Star points */}
|
||||
<polygon points="32,2 38,22 58,22 42,36 48,56 32,44 16,56 22,36 6,22 26,22" fill={colors.primary} />
|
||||
<circle cx="32" cy="32" r="14" fill={colors.secondary} />
|
||||
{/* Face */}
|
||||
<circle cx="27" cy="30" r="3" fill="white" />
|
||||
<circle cx="37" cy="30" r="3" fill="white" />
|
||||
<circle cx="28" cy="31" r="1.5" fill="#1a1a1a" />
|
||||
<circle cx="38" cy="31" r="1.5" fill="#1a1a1a" />
|
||||
<path d="M28,37 Q32,40 36,37" stroke="#1a1a1a" strokeWidth="1.5" fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Chip - circuit board character
|
||||
function ChipSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Chip; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Chip body */}
|
||||
<rect x="16" y="16" width="32" height="32" rx="4" fill={colors.primary} />
|
||||
{/* Pins */}
|
||||
<rect x="20" y="10" width="4" height="8" fill={colors.secondary} />
|
||||
<rect x="30" y="10" width="4" height="8" fill={colors.secondary} />
|
||||
<rect x="40" y="10" width="4" height="8" fill={colors.secondary} />
|
||||
<rect x="20" y="46" width="4" height="8" fill={colors.secondary} />
|
||||
<rect x="30" y="46" width="4" height="8" fill={colors.secondary} />
|
||||
<rect x="40" y="46" width="4" height="8" fill={colors.secondary} />
|
||||
{/* Face */}
|
||||
<circle cx="26" cy="28" r="4" fill={colors.accent} />
|
||||
<circle cx="38" cy="28" r="4" fill={colors.accent} />
|
||||
<circle cx="26" cy="28" r="2" fill="#1a1a1a" />
|
||||
<circle cx="38" cy="28" r="2" fill="#1a1a1a" />
|
||||
<rect x="26" y="38" width="12" height="3" rx="1" fill={colors.accent} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Bolt - lightning character
|
||||
function BoltSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Bolt; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Lightning bolt body */}
|
||||
<polygon points="36,4 20,28 30,28 24,60 48,32 36,32 44,4" fill={colors.primary} />
|
||||
<polygon points="34,8 24,26 32,26 28,52 42,34 34,34 40,8" fill={colors.secondary} />
|
||||
{/* Face */}
|
||||
<circle cx="30" cy="30" r="3" fill="white" />
|
||||
<circle cx="38" cy="26" r="3" fill="white" />
|
||||
<circle cx="31" cy="31" r="1.5" fill="#1a1a1a" />
|
||||
<circle cx="39" cy="27" r="1.5" fill="#1a1a1a" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Dash - speedy character
|
||||
function DashSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Dash; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Speed lines */}
|
||||
<rect x="4" y="28" width="12" height="3" rx="1" fill={colors.accent} opacity="0.6" />
|
||||
<rect x="8" y="34" width="10" height="3" rx="1" fill={colors.accent} opacity="0.4" />
|
||||
{/* Aerodynamic body */}
|
||||
<ellipse cx="36" cy="32" rx="20" ry="16" fill={colors.primary} />
|
||||
<ellipse cx="40" cy="32" rx="14" ry="12" fill={colors.secondary} />
|
||||
{/* Face */}
|
||||
<circle cx="38" cy="28" r="4" fill="white" />
|
||||
<circle cx="48" cy="28" r="4" fill="white" />
|
||||
<circle cx="39" cy="29" r="2" fill="#1a1a1a" />
|
||||
<circle cx="49" cy="29" r="2" fill="#1a1a1a" />
|
||||
<path d="M40,36 Q44,39 48,36" stroke="#1a1a1a" strokeWidth="1.5" fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Zap - electric orb
|
||||
function ZapSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Zap; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Electric sparks */}
|
||||
<path d="M12,32 L20,28 L16,32 L22,30" stroke={colors.secondary} strokeWidth="2" className="animate-pulse" />
|
||||
<path d="M52,32 L44,28 L48,32 L42,30" stroke={colors.secondary} strokeWidth="2" className="animate-pulse" />
|
||||
{/* Orb */}
|
||||
<circle cx="32" cy="32" r="18" fill={colors.primary} />
|
||||
<circle cx="32" cy="32" r="14" fill={colors.secondary} />
|
||||
{/* Face */}
|
||||
<circle cx="26" cy="30" r="4" fill="white" />
|
||||
<circle cx="38" cy="30" r="4" fill="white" />
|
||||
<circle cx="27" cy="31" r="2" fill={colors.primary} />
|
||||
<circle cx="39" cy="31" r="2" fill={colors.primary} />
|
||||
<path d="M28,40 Q32,44 36,40" stroke="white" strokeWidth="2" fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Gizmo - gear character
|
||||
function GizmoSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Gizmo; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Gear teeth */}
|
||||
<rect x="28" y="4" width="8" height="8" fill={colors.primary} />
|
||||
<rect x="28" y="52" width="8" height="8" fill={colors.primary} />
|
||||
<rect x="4" y="28" width="8" height="8" fill={colors.primary} />
|
||||
<rect x="52" y="28" width="8" height="8" fill={colors.primary} />
|
||||
{/* Gear body */}
|
||||
<circle cx="32" cy="32" r="20" fill={colors.primary} />
|
||||
<circle cx="32" cy="32" r="14" fill={colors.secondary} />
|
||||
{/* Face */}
|
||||
<circle cx="26" cy="30" r="4" fill="white" />
|
||||
<circle cx="38" cy="30" r="4" fill="white" />
|
||||
<circle cx="27" cy="31" r="2" fill="#1a1a1a" />
|
||||
<circle cx="39" cy="31" r="2" fill="#1a1a1a" />
|
||||
<path d="M28,40 Q32,43 36,40" stroke="#1a1a1a" strokeWidth="2" fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Turbo - rocket character
|
||||
function TurboSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Turbo; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Flames */}
|
||||
<ellipse cx="32" cy="58" rx="8" ry="6" fill="#FBBF24" className="animate-pulse" />
|
||||
<ellipse cx="32" cy="56" rx="5" ry="4" fill="#FCD34D" />
|
||||
{/* Rocket body */}
|
||||
<ellipse cx="32" cy="32" rx="14" ry="24" fill={colors.primary} />
|
||||
{/* Nose cone */}
|
||||
<ellipse cx="32" cy="12" rx="8" ry="10" fill={colors.secondary} />
|
||||
{/* Fins */}
|
||||
<polygon points="18,44 10,56 18,52" fill={colors.secondary} />
|
||||
<polygon points="46,44 54,56 46,52" fill={colors.secondary} />
|
||||
{/* Window/Face */}
|
||||
<circle cx="32" cy="28" r="8" fill={colors.accent} />
|
||||
<circle cx="29" cy="27" r="2" fill="#1a1a1a" />
|
||||
<circle cx="35" cy="27" r="2" fill="#1a1a1a" />
|
||||
<path d="M29,32 Q32,34 35,32" stroke="#1a1a1a" strokeWidth="1" fill="none" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Blip - radar dot character
|
||||
function BlipSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Blip; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Radar rings */}
|
||||
<circle cx="32" cy="32" r="28" stroke={colors.accent} strokeWidth="2" fill="none" opacity="0.3" />
|
||||
<circle cx="32" cy="32" r="22" stroke={colors.accent} strokeWidth="2" fill="none" opacity="0.5" />
|
||||
{/* Main dot */}
|
||||
<circle cx="32" cy="32" r="14" fill={colors.primary} />
|
||||
<circle cx="32" cy="32" r="10" fill={colors.secondary} />
|
||||
{/* Face */}
|
||||
<circle cx="28" cy="30" r="3" fill="white" />
|
||||
<circle cx="36" cy="30" r="3" fill="white" />
|
||||
<circle cx="29" cy="31" r="1.5" fill="#1a1a1a" />
|
||||
<circle cx="37" cy="31" r="1.5" fill="#1a1a1a" />
|
||||
<path d="M29,37 Q32,40 35,37" stroke="white" strokeWidth="1.5" fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Neon - glowing character
|
||||
function NeonSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Neon; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Glow effect */}
|
||||
<circle cx="32" cy="32" r="26" fill={colors.accent} opacity="0.3" />
|
||||
<circle cx="32" cy="32" r="22" fill={colors.accent} opacity="0.5" />
|
||||
{/* Body */}
|
||||
<circle cx="32" cy="32" r="18" fill={colors.primary} />
|
||||
{/* Inner glow */}
|
||||
<circle cx="32" cy="32" r="12" fill={colors.secondary} />
|
||||
{/* Face */}
|
||||
<circle cx="27" cy="30" r="4" fill="white" />
|
||||
<circle cx="37" cy="30" r="4" fill="white" />
|
||||
<circle cx="28" cy="31" r="2" fill={colors.primary} />
|
||||
<circle cx="38" cy="31" r="2" fill={colors.primary} />
|
||||
<path d="M28,38 Q32,42 36,38" stroke="white" strokeWidth="2" fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Widget - UI component character
|
||||
function WidgetSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Widget; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Window frame */}
|
||||
<rect x="8" y="12" width="48" height="40" rx="4" fill={colors.primary} />
|
||||
{/* Title bar */}
|
||||
<rect x="8" y="12" width="48" height="10" rx="4" fill={colors.secondary} />
|
||||
<circle cx="16" cy="17" r="2" fill="#EF4444" />
|
||||
<circle cx="24" cy="17" r="2" fill="#FBBF24" />
|
||||
<circle cx="32" cy="17" r="2" fill="#22C55E" />
|
||||
{/* Content area / Face */}
|
||||
<rect x="12" y="26" width="40" height="22" rx="2" fill={colors.accent} />
|
||||
<circle cx="24" cy="34" r="4" fill="white" />
|
||||
<circle cx="40" cy="34" r="4" fill="white" />
|
||||
<circle cx="25" cy="35" r="2" fill={colors.primary} />
|
||||
<circle cx="41" cy="35" r="2" fill={colors.primary} />
|
||||
<rect x="28" y="42" width="8" height="3" rx="1" fill={colors.primary} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Zippy - fast bunny-like character
|
||||
function ZippySVG({ colors, size }: { colors: typeof AVATAR_COLORS.Zippy; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Ears */}
|
||||
<ellipse cx="22" cy="14" rx="6" ry="14" fill={colors.primary} />
|
||||
<ellipse cx="42" cy="14" rx="6" ry="14" fill={colors.primary} />
|
||||
<ellipse cx="22" cy="14" rx="3" ry="10" fill={colors.accent} />
|
||||
<ellipse cx="42" cy="14" rx="3" ry="10" fill={colors.accent} />
|
||||
{/* Head */}
|
||||
<circle cx="32" cy="38" r="20" fill={colors.primary} />
|
||||
{/* Face */}
|
||||
<circle cx="24" cy="34" r="5" fill="white" />
|
||||
<circle cx="40" cy="34" r="5" fill="white" />
|
||||
<circle cx="25" cy="35" r="2.5" fill="#1a1a1a" />
|
||||
<circle cx="41" cy="35" r="2.5" fill="#1a1a1a" />
|
||||
{/* Nose and mouth */}
|
||||
<ellipse cx="32" cy="44" rx="3" ry="2" fill={colors.secondary} />
|
||||
<path d="M32,46 L32,50 M28,52 Q32,56 36,52" stroke="#1a1a1a" strokeWidth="1.5" fill="none" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Quirk - question mark character
|
||||
function QuirkSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Quirk; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Question mark body */}
|
||||
<path d="M24,20 Q24,8 32,8 Q44,8 44,20 Q44,28 32,32 L32,40"
|
||||
stroke={colors.primary} strokeWidth="8" fill="none" strokeLinecap="round" />
|
||||
<circle cx="32" cy="52" r="6" fill={colors.primary} />
|
||||
{/* Face on the dot */}
|
||||
<circle cx="29" cy="51" r="1.5" fill="white" />
|
||||
<circle cx="35" cy="51" r="1.5" fill="white" />
|
||||
<circle cx="29" cy="51" r="0.75" fill="#1a1a1a" />
|
||||
<circle cx="35" cy="51" r="0.75" fill="#1a1a1a" />
|
||||
{/* Decorative swirl */}
|
||||
<circle cx="32" cy="20" r="4" fill={colors.secondary} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Flux - flowing wave character
|
||||
function FluxSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Flux; size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
{/* Wave body */}
|
||||
<path d="M8,32 Q16,16 32,32 Q48,48 56,32" stroke={colors.primary} strokeWidth="16" fill="none" strokeLinecap="round" />
|
||||
<path d="M8,32 Q16,16 32,32 Q48,48 56,32" stroke={colors.secondary} strokeWidth="10" fill="none" strokeLinecap="round" />
|
||||
{/* Face */}
|
||||
<circle cx="28" cy="28" r="4" fill="white" />
|
||||
<circle cx="40" cy="36" r="4" fill="white" />
|
||||
<circle cx="29" cy="29" r="2" fill="#1a1a1a" />
|
||||
<circle cx="41" cy="37" r="2" fill="#1a1a1a" />
|
||||
{/* Sparkles */}
|
||||
<circle cx="16" cy="24" r="2" fill={colors.accent} className="animate-pulse" />
|
||||
<circle cx="48" cy="40" r="2" fill={colors.accent} className="animate-pulse" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const MASCOT_SVGS: Record<AgentMascot, typeof SparkSVG> = {
|
||||
// Original 5
|
||||
Spark: SparkSVG,
|
||||
Fizz: FizzSVG,
|
||||
Octo: OctoSVG,
|
||||
Hoot: HootSVG,
|
||||
Buzz: BuzzSVG,
|
||||
// Tech-inspired
|
||||
Pixel: PixelSVG,
|
||||
Byte: ByteSVG,
|
||||
Nova: NovaSVG,
|
||||
Chip: ChipSVG,
|
||||
Bolt: BoltSVG,
|
||||
// Energetic
|
||||
Dash: DashSVG,
|
||||
Zap: ZapSVG,
|
||||
Gizmo: GizmoSVG,
|
||||
Turbo: TurboSVG,
|
||||
Blip: BlipSVG,
|
||||
// Playful
|
||||
Neon: NeonSVG,
|
||||
Widget: WidgetSVG,
|
||||
Zippy: ZippySVG,
|
||||
Quirk: QuirkSVG,
|
||||
Flux: FluxSVG,
|
||||
}
|
||||
|
||||
// Animation classes based on state
|
||||
@@ -256,6 +598,6 @@ export function AgentAvatar({ name, state, size = 'md', showName = false }: Agen
|
||||
|
||||
// Get mascot name by index (cycles through available mascots)
|
||||
export function getMascotName(index: number): AgentMascot {
|
||||
const mascots: AgentMascot[] = ['Spark', 'Fizz', 'Octo', 'Hoot', 'Buzz']
|
||||
const mascots = Object.keys(MASCOT_SVGS) as AgentMascot[]
|
||||
return mascots[index % mascots.length]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MessageCircle, ScrollText, X, Copy, Check } from 'lucide-react'
|
||||
import { MessageCircle, ScrollText, X, Copy, Check, Code, FlaskConical } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { AgentAvatar } from './AgentAvatar'
|
||||
import type { ActiveAgent, AgentLogEntry } from '../lib/types'
|
||||
import type { ActiveAgent, AgentLogEntry, AgentType } from '../lib/types'
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: ActiveAgent
|
||||
@@ -50,9 +50,28 @@ function getStateColor(state: ActiveAgent['state']): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Get agent type badge config
|
||||
function getAgentTypeBadge(agentType: AgentType): { label: string; className: string; icon: typeof Code } {
|
||||
if (agentType === 'testing') {
|
||||
return {
|
||||
label: 'TEST',
|
||||
className: 'bg-purple-100 text-purple-700 border-purple-300',
|
||||
icon: FlaskConical,
|
||||
}
|
||||
}
|
||||
// Default to coding
|
||||
return {
|
||||
label: 'CODE',
|
||||
className: 'bg-blue-100 text-blue-700 border-blue-300',
|
||||
icon: Code,
|
||||
}
|
||||
}
|
||||
|
||||
export function AgentCard({ agent, onShowLogs }: AgentCardProps) {
|
||||
const isActive = ['thinking', 'working', 'testing'].includes(agent.state)
|
||||
const hasLogs = agent.logs && agent.logs.length > 0
|
||||
const typeBadge = getAgentTypeBadge(agent.agentType || 'coding')
|
||||
const TypeIcon = typeBadge.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -62,6 +81,20 @@ export function AgentCard({ agent, onShowLogs }: AgentCardProps) {
|
||||
transition-all duration-300
|
||||
`}
|
||||
>
|
||||
{/* Agent type badge */}
|
||||
<div className="flex justify-end mb-1">
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-bold
|
||||
uppercase tracking-wide rounded border
|
||||
${typeBadge.className}
|
||||
`}
|
||||
>
|
||||
<TypeIcon size={10} />
|
||||
{typeBadge.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Header with avatar and name */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AgentAvatar name={agent.agentName} state={agent.state} size="sm" />
|
||||
@@ -122,6 +155,8 @@ interface AgentLogModalProps {
|
||||
|
||||
export function AgentLogModal({ agent, logs, onClose }: AgentLogModalProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const typeBadge = getAgentTypeBadge(agent.agentType || 'coding')
|
||||
const TypeIcon = typeBadge.icon
|
||||
|
||||
const handleCopy = async () => {
|
||||
const logText = logs
|
||||
@@ -159,9 +194,21 @@ export function AgentLogModal({ agent, logs, onClose }: AgentLogModalProps) {
|
||||
<div className="flex items-center gap-3">
|
||||
<AgentAvatar name={agent.agentName} state={agent.state} size="sm" />
|
||||
<div>
|
||||
<h2 className="font-display font-bold text-lg">
|
||||
{agent.agentName} Logs
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-display font-bold text-lg">
|
||||
{agent.agentName} Logs
|
||||
</h2>
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-bold
|
||||
uppercase tracking-wide rounded border
|
||||
${typeBadge.className}
|
||||
`}
|
||||
>
|
||||
<TypeIcon size={10} />
|
||||
{typeBadge.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-neo-text-secondary">
|
||||
Feature #{agent.featureId}: {agent.featureName}
|
||||
</p>
|
||||
|
||||
@@ -24,21 +24,24 @@ export function AgentControl({ projectName, status }: AgentControlProps) {
|
||||
|
||||
const isLoading = startAgent.isPending || stopAgent.isPending
|
||||
const isRunning = status === 'running' || status === 'paused'
|
||||
const isLoadingStatus = status === 'loading' // Status unknown, waiting for WebSocket
|
||||
const isParallel = concurrency > 1
|
||||
|
||||
const handleStart = () => startAgent.mutate({
|
||||
yoloMode,
|
||||
parallelMode: isParallel,
|
||||
maxConcurrency: isParallel ? concurrency : undefined,
|
||||
maxConcurrency: concurrency, // Always pass concurrency (1-5)
|
||||
testingAgentRatio: settings?.testing_agent_ratio,
|
||||
countTestingInConcurrency: settings?.count_testing_in_concurrency,
|
||||
})
|
||||
const handleStop = () => stopAgent.mutate()
|
||||
|
||||
// Simplified: either show Start (when stopped/crashed) or Stop (when running/paused)
|
||||
// Simplified: either show Start (when stopped/crashed), Stop (when running/paused), or loading spinner
|
||||
const isStopped = status === 'stopped' || status === 'crashed'
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Concurrency slider - always visible when stopped */}
|
||||
{/* Concurrency slider - visible when stopped (not during loading or running) */}
|
||||
{isStopped && (
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch size={16} className={isParallel ? 'text-[var(--color-neo-primary)]' : 'text-gray-400'} />
|
||||
@@ -67,7 +70,16 @@ export function AgentControl({ projectName, status }: AgentControlProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isStopped ? (
|
||||
{isLoadingStatus ? (
|
||||
<button
|
||||
disabled
|
||||
className="neo-btn text-sm py-2 px-3 opacity-50 cursor-not-allowed"
|
||||
title="Loading agent status..."
|
||||
aria-label="Loading agent status"
|
||||
>
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
</button>
|
||||
) : isStopped ? (
|
||||
<button
|
||||
onClick={handleStart}
|
||||
disabled={isLoading}
|
||||
|
||||
@@ -129,7 +129,11 @@ export function NewProjectModal({
|
||||
// Auto-start the initializer agent
|
||||
setInitializerStatus('starting')
|
||||
try {
|
||||
await startAgent(projectName.trim(), { yoloMode })
|
||||
// Use default concurrency of 3 to match AgentControl.tsx default
|
||||
await startAgent(projectName.trim(), {
|
||||
yoloMode,
|
||||
maxConcurrency: 3,
|
||||
})
|
||||
// Success - navigate to project
|
||||
changeStep('complete')
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -70,6 +70,18 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTestingRatioChange = (ratio: number) => {
|
||||
if (!updateSettings.isPending) {
|
||||
updateSettings.mutate({ testing_agent_ratio: ratio })
|
||||
}
|
||||
}
|
||||
|
||||
const handleCountTestingToggle = () => {
|
||||
if (settings && !updateSettings.isPending) {
|
||||
updateSettings.mutate({ count_testing_in_concurrency: !settings.count_testing_in_concurrency })
|
||||
}
|
||||
}
|
||||
|
||||
const models = modelsData?.models ?? []
|
||||
const isSaving = updateSettings.isPending
|
||||
|
||||
@@ -199,6 +211,76 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testing Agent Ratio */}
|
||||
<div>
|
||||
<label
|
||||
id="testing-ratio-label"
|
||||
className="font-display font-bold text-base block mb-1"
|
||||
>
|
||||
Testing Agents per Coding Agent
|
||||
</label>
|
||||
<p className="text-sm text-[var(--color-neo-text-secondary)] mb-2">
|
||||
Regression testing agents spawned per coding agent (0 = disabled)
|
||||
</p>
|
||||
<div
|
||||
className="flex border-3 border-[var(--color-neo-border)]"
|
||||
role="radiogroup"
|
||||
aria-labelledby="testing-ratio-label"
|
||||
>
|
||||
{[0, 1, 2, 3].map((ratio) => (
|
||||
<button
|
||||
key={ratio}
|
||||
onClick={() => handleTestingRatioChange(ratio)}
|
||||
disabled={isSaving}
|
||||
role="radio"
|
||||
aria-checked={settings.testing_agent_ratio === ratio}
|
||||
className={`flex-1 py-2 px-3 font-display font-bold text-sm transition-colors ${
|
||||
settings.testing_agent_ratio === ratio
|
||||
? 'bg-[var(--color-neo-progress)] text-[var(--color-neo-text)]'
|
||||
: 'bg-[var(--color-neo-card)] text-[var(--color-neo-text)] hover:bg-[var(--color-neo-hover-subtle)]'
|
||||
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{ratio}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Count Testing in Concurrency Toggle */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label
|
||||
id="count-testing-label"
|
||||
className="font-display font-bold text-base"
|
||||
>
|
||||
Count Testing in Concurrency
|
||||
</label>
|
||||
<p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
|
||||
If enabled, testing agents count toward the concurrency limit
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCountTestingToggle}
|
||||
disabled={isSaving}
|
||||
className={`relative w-14 h-8 rounded-none border-3 border-[var(--color-neo-border)] transition-colors ${
|
||||
settings.count_testing_in_concurrency
|
||||
? 'bg-[var(--color-neo-progress)]'
|
||||
: 'bg-[var(--color-neo-card)]'
|
||||
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
role="switch"
|
||||
aria-checked={settings.count_testing_in_concurrency}
|
||||
aria-labelledby="count-testing-label"
|
||||
>
|
||||
<span
|
||||
className={`absolute top-1 w-5 h-5 bg-[var(--color-neo-border)] transition-transform ${
|
||||
settings.count_testing_in_concurrency ? 'left-7' : 'left-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Update Error */}
|
||||
{updateSettings.isError && (
|
||||
<div className="p-3 bg-[var(--color-neo-error-bg)] border-3 border-[var(--color-neo-error-border)] text-[var(--color-neo-error-text)] text-sm">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip, ExternalLink } from 'lucide-react'
|
||||
import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip, ExternalLink, FileText } from 'lucide-react'
|
||||
import { useSpecChat } from '../hooks/useSpecChat'
|
||||
import { ChatMessage } from './ChatMessage'
|
||||
import { QuestionOptions } from './QuestionOptions'
|
||||
@@ -17,6 +17,24 @@ import type { ImageAttachment } from '../lib/types'
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
|
||||
const ALLOWED_TYPES = ['image/jpeg', 'image/png']
|
||||
|
||||
// Sample prompt for quick testing
|
||||
const SAMPLE_PROMPT = `Let's call it Simple Todo. This is a really simple web app that I can use to track my to-do items using a Kanban board. I should be able to add to-dos and then drag and drop them through the Kanban board. The different columns in the Kanban board are:
|
||||
|
||||
- To Do
|
||||
- In Progress
|
||||
- Done
|
||||
|
||||
The app should use a neobrutalism design.
|
||||
|
||||
There is no need for user authentication either. All the to-dos will be stored in local storage, so each user has access to all of their to-dos when they open their browser. So do not worry about implementing a backend with user authentication or a database. Simply store everything in local storage. As for the design, please try to avoid AI slop, so use your front-end design skills to design something beautiful and practical. As for the content of the to-dos, we should store:
|
||||
|
||||
- The name or the title at the very least
|
||||
- Optionally, we can also set tags, due dates, and priorities which should be represented as beautiful little badges on the to-do card
|
||||
|
||||
Users should have the ability to easily clear out all the completed To-Dos. They should also be able to filter and search for To-Dos as well.
|
||||
|
||||
You choose the rest. Keep it simple. Should be 25 features.`
|
||||
|
||||
type InitializerStatus = 'idle' | 'starting' | 'error'
|
||||
|
||||
interface SpecCreationChatProps {
|
||||
@@ -223,6 +241,23 @@ export function SpecCreationChat({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Load Sample Prompt */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setInput(SAMPLE_PROMPT)
|
||||
// Also resize the textarea to fit content
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.height = 'auto'
|
||||
inputRef.current.style.height = `${Math.min(inputRef.current.scrollHeight, 200)}px`
|
||||
}
|
||||
}}
|
||||
className="neo-btn neo-btn-ghost text-sm py-2"
|
||||
title="Load sample prompt (Simple Todo app)"
|
||||
>
|
||||
<FileText size={16} />
|
||||
Load Sample
|
||||
</button>
|
||||
|
||||
{/* Exit to Project - always visible escape hatch */}
|
||||
<button
|
||||
onClick={onExitToProject}
|
||||
|
||||
@@ -127,6 +127,8 @@ export function useStartAgent(projectName: string) {
|
||||
yoloMode?: boolean
|
||||
parallelMode?: boolean
|
||||
maxConcurrency?: number
|
||||
testingAgentRatio?: number
|
||||
countTestingInConcurrency?: boolean
|
||||
} = {}) => api.startAgent(projectName, options),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
|
||||
@@ -234,6 +236,8 @@ const DEFAULT_SETTINGS: Settings = {
|
||||
yolo_mode: false,
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
glm_mode: false,
|
||||
testing_agent_ratio: 1,
|
||||
count_testing_in_concurrency: false,
|
||||
}
|
||||
|
||||
export function useAvailableModels() {
|
||||
|
||||
@@ -57,7 +57,7 @@ const MAX_AGENT_LOGS = 500 // Keep last 500 log lines per agent
|
||||
export function useProjectWebSocket(projectName: string | null) {
|
||||
const [state, setState] = useState<WebSocketState>({
|
||||
progress: { passing: 0, in_progress: 0, total: 0, percentage: 0 },
|
||||
agentStatus: 'stopped',
|
||||
agentStatus: 'loading',
|
||||
logs: [],
|
||||
isConnected: false,
|
||||
devServerStatus: 'stopped',
|
||||
@@ -188,6 +188,7 @@ export function useProjectWebSocket(projectName: string | null) {
|
||||
newAgents[existingAgentIdx] = {
|
||||
agentIndex: message.agentIndex,
|
||||
agentName: message.agentName,
|
||||
agentType: message.agentType || 'coding', // Default to coding for backwards compat
|
||||
featureId: message.featureId,
|
||||
featureName: message.featureName,
|
||||
state: message.state,
|
||||
@@ -202,6 +203,7 @@ export function useProjectWebSocket(projectName: string | null) {
|
||||
{
|
||||
agentIndex: message.agentIndex,
|
||||
agentName: message.agentName,
|
||||
agentType: message.agentType || 'coding', // Default to coding for backwards compat
|
||||
featureId: message.featureId,
|
||||
featureName: message.featureName,
|
||||
state: message.state,
|
||||
@@ -328,9 +330,10 @@ export function useProjectWebSocket(projectName: string | null) {
|
||||
// Connect when project changes
|
||||
useEffect(() => {
|
||||
// Reset state when project changes to clear stale data
|
||||
// Use 'loading' for agentStatus to show loading indicator until WebSocket provides actual status
|
||||
setState({
|
||||
progress: { passing: 0, in_progress: 0, total: 0, percentage: 0 },
|
||||
agentStatus: 'stopped',
|
||||
agentStatus: 'loading',
|
||||
logs: [],
|
||||
isConnected: false,
|
||||
devServerStatus: 'stopped',
|
||||
|
||||
@@ -200,6 +200,8 @@ export async function startAgent(
|
||||
yoloMode?: boolean
|
||||
parallelMode?: boolean
|
||||
maxConcurrency?: number
|
||||
testingAgentRatio?: number
|
||||
countTestingInConcurrency?: boolean
|
||||
} = {}
|
||||
): Promise<AgentActionResponse> {
|
||||
return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/start`, {
|
||||
@@ -208,6 +210,8 @@ export async function startAgent(
|
||||
yolo_mode: options.yoloMode ?? false,
|
||||
parallel_mode: options.parallelMode ?? false,
|
||||
max_concurrency: options.maxConcurrency,
|
||||
testing_agent_ratio: options.testingAgentRatio,
|
||||
count_testing_in_concurrency: options.countTestingInConcurrency,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export interface FeatureUpdate {
|
||||
}
|
||||
|
||||
// Agent types
|
||||
export type AgentStatus = 'stopped' | 'running' | 'paused' | 'crashed'
|
||||
export type AgentStatus = 'stopped' | 'running' | 'paused' | 'crashed' | 'loading'
|
||||
|
||||
export interface AgentStatusResponse {
|
||||
status: AgentStatus
|
||||
@@ -127,8 +127,10 @@ export interface AgentStatusResponse {
|
||||
started_at: string | null
|
||||
yolo_mode: boolean
|
||||
model: string | null // Model being used by running agent
|
||||
parallel_mode: boolean
|
||||
parallel_mode: boolean // DEPRECATED: Always true now (unified orchestrator)
|
||||
max_concurrency: number | null
|
||||
testing_agent_ratio: number // Testing agents per coding agent (0-3)
|
||||
count_testing_in_concurrency: boolean // Count testing toward concurrency limit
|
||||
}
|
||||
|
||||
export interface AgentActionResponse {
|
||||
@@ -171,12 +173,20 @@ export interface TerminalInfo {
|
||||
}
|
||||
|
||||
// Agent mascot names for multi-agent UI
|
||||
export const AGENT_MASCOTS = ['Spark', 'Fizz', 'Octo', 'Hoot', 'Buzz'] as const
|
||||
export const AGENT_MASCOTS = [
|
||||
'Spark', 'Fizz', 'Octo', 'Hoot', 'Buzz', // Original 5
|
||||
'Pixel', 'Byte', 'Nova', 'Chip', 'Bolt', // Tech-inspired
|
||||
'Dash', 'Zap', 'Gizmo', 'Turbo', 'Blip', // Energetic
|
||||
'Neon', 'Widget', 'Zippy', 'Quirk', 'Flux', // Playful
|
||||
] as const
|
||||
export type AgentMascot = typeof AGENT_MASCOTS[number]
|
||||
|
||||
// Agent state for Mission Control
|
||||
export type AgentState = 'idle' | 'thinking' | 'working' | 'testing' | 'success' | 'error' | 'struggling'
|
||||
|
||||
// Agent type (coding vs testing)
|
||||
export type AgentType = 'coding' | 'testing'
|
||||
|
||||
// Individual log entry for an agent
|
||||
export interface AgentLogEntry {
|
||||
line: string
|
||||
@@ -188,6 +198,7 @@ export interface AgentLogEntry {
|
||||
export interface ActiveAgent {
|
||||
agentIndex: number
|
||||
agentName: AgentMascot
|
||||
agentType: AgentType // "coding" or "testing"
|
||||
featureId: number
|
||||
featureName: string
|
||||
state: AgentState
|
||||
@@ -226,6 +237,7 @@ export interface WSAgentUpdateMessage {
|
||||
type: 'agent_update'
|
||||
agentIndex: number
|
||||
agentName: AgentMascot
|
||||
agentType: AgentType // "coding" or "testing"
|
||||
featureId: number
|
||||
featureName: string
|
||||
state: AgentState
|
||||
@@ -467,9 +479,13 @@ export interface Settings {
|
||||
yolo_mode: boolean
|
||||
model: string
|
||||
glm_mode: boolean
|
||||
testing_agent_ratio: number // Testing agents per coding agent (0-3)
|
||||
count_testing_in_concurrency: boolean // Count testing toward concurrency limit
|
||||
}
|
||||
|
||||
export interface SettingsUpdate {
|
||||
yolo_mode?: boolean
|
||||
model?: string
|
||||
testing_agent_ratio?: number
|
||||
count_testing_in_concurrency?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user