mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 14:22:04 +00:00
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>
296 lines
11 KiB
TypeScript
296 lines
11 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import { X, Loader2, AlertCircle } from 'lucide-react'
|
|
import { useSettings, useUpdateSettings, useAvailableModels } from '../hooks/useProjects'
|
|
|
|
interface SettingsModalProps {
|
|
onClose: () => void
|
|
}
|
|
|
|
export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
const { data: settings, isLoading, isError, refetch } = useSettings()
|
|
const { data: modelsData } = useAvailableModels()
|
|
const updateSettings = useUpdateSettings()
|
|
const modalRef = useRef<HTMLDivElement>(null)
|
|
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
|
|
|
// Focus trap - keep focus within modal
|
|
useEffect(() => {
|
|
const modal = modalRef.current
|
|
if (!modal) return
|
|
|
|
// Focus the close button when modal opens
|
|
closeButtonRef.current?.focus()
|
|
|
|
const focusableElements = modal.querySelectorAll<HTMLElement>(
|
|
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
)
|
|
const firstElement = focusableElements[0]
|
|
const lastElement = focusableElements[focusableElements.length - 1]
|
|
|
|
const handleTabKey = (e: KeyboardEvent) => {
|
|
if (e.key !== 'Tab') return
|
|
|
|
if (e.shiftKey) {
|
|
if (document.activeElement === firstElement) {
|
|
e.preventDefault()
|
|
lastElement?.focus()
|
|
}
|
|
} else {
|
|
if (document.activeElement === lastElement) {
|
|
e.preventDefault()
|
|
firstElement?.focus()
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
onClose()
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', handleTabKey)
|
|
document.addEventListener('keydown', handleEscape)
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleTabKey)
|
|
document.removeEventListener('keydown', handleEscape)
|
|
}
|
|
}, [onClose])
|
|
|
|
const handleYoloToggle = () => {
|
|
if (settings && !updateSettings.isPending) {
|
|
updateSettings.mutate({ yolo_mode: !settings.yolo_mode })
|
|
}
|
|
}
|
|
|
|
const handleModelChange = (modelId: string) => {
|
|
if (!updateSettings.isPending) {
|
|
updateSettings.mutate({ model: modelId })
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
return (
|
|
<div
|
|
className="neo-modal-backdrop"
|
|
onClick={onClose}
|
|
role="presentation"
|
|
>
|
|
<div
|
|
ref={modalRef}
|
|
className="neo-modal w-full max-w-sm p-6"
|
|
onClick={(e) => e.stopPropagation()}
|
|
role="dialog"
|
|
aria-labelledby="settings-title"
|
|
aria-modal="true"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 id="settings-title" className="font-display text-xl font-bold">
|
|
Settings
|
|
{isSaving && (
|
|
<Loader2 className="inline-block ml-2 animate-spin" size={16} />
|
|
)}
|
|
</h2>
|
|
<button
|
|
ref={closeButtonRef}
|
|
onClick={onClose}
|
|
className="neo-btn neo-btn-ghost p-2"
|
|
aria-label="Close settings"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Loading State */}
|
|
{isLoading && (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="animate-spin" size={24} />
|
|
<span className="ml-2">Loading settings...</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error State */}
|
|
{isError && (
|
|
<div className="p-4 bg-[var(--color-neo-error-bg)] text-[var(--color-neo-error-text)] border-3 border-[var(--color-neo-error-border)] mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<AlertCircle size={18} />
|
|
<span>Failed to load settings</span>
|
|
</div>
|
|
<button
|
|
onClick={() => refetch()}
|
|
className="mt-2 underline text-sm hover:opacity-70 transition-opacity"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Settings Content */}
|
|
{settings && !isLoading && (
|
|
<div className="space-y-6">
|
|
{/* YOLO Mode Toggle */}
|
|
<div>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<label
|
|
id="yolo-label"
|
|
className="font-display font-bold text-base"
|
|
>
|
|
YOLO Mode
|
|
</label>
|
|
<p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
|
|
Skip testing for rapid prototyping
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleYoloToggle}
|
|
disabled={isSaving}
|
|
className={`relative w-14 h-8 rounded-none border-3 border-[var(--color-neo-border)] transition-colors ${
|
|
settings.yolo_mode
|
|
? 'bg-[var(--color-neo-pending)]'
|
|
: 'bg-[var(--color-neo-card)]'
|
|
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
role="switch"
|
|
aria-checked={settings.yolo_mode}
|
|
aria-labelledby="yolo-label"
|
|
>
|
|
<span
|
|
className={`absolute top-1 w-5 h-5 bg-[var(--color-neo-border)] transition-transform ${
|
|
settings.yolo_mode ? 'left-7' : 'left-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Model Selection - Radio Group */}
|
|
<div>
|
|
<label
|
|
id="model-label"
|
|
className="font-display font-bold text-base block mb-2"
|
|
>
|
|
Model
|
|
</label>
|
|
<div
|
|
className="flex border-3 border-[var(--color-neo-border)]"
|
|
role="radiogroup"
|
|
aria-labelledby="model-label"
|
|
>
|
|
{models.map((model) => (
|
|
<button
|
|
key={model.id}
|
|
onClick={() => handleModelChange(model.id)}
|
|
disabled={isSaving}
|
|
role="radio"
|
|
aria-checked={settings.model === model.id}
|
|
className={`flex-1 py-3 px-4 font-display font-bold text-sm transition-colors ${
|
|
settings.model === model.id
|
|
? 'bg-[var(--color-neo-accent)] text-[var(--color-neo-text-on-bright)]'
|
|
: 'bg-[var(--color-neo-card)] text-[var(--color-neo-text)] hover:bg-[var(--color-neo-hover-subtle)]'
|
|
} ${isSaving ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
>
|
|
{model.name}
|
|
</button>
|
|
))}
|
|
</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">
|
|
Failed to save settings. Please try again.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|