Files
autocoder/ui/src/components/SettingsModal.tsx
Auto 13128361b0 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>
2026-01-18 13:49:50 +02:00

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>
)
}