mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
feat: Add global settings modal and simplify agent controls
Adds a settings system for global configuration with YOLO mode toggle and
model selection. Simplifies the agent control UI by removing redundant
status indicator and pause functionality.
## Settings System
- New SettingsModal with YOLO mode toggle and model selection
- Settings persisted in SQLite (registry.db) - shared across all projects
- Models fetched from API endpoint (/api/settings/models)
- Single source of truth for models in registry.py - easy to add new models
- Optimistic UI updates with rollback on error
## Agent Control Simplification
- Removed StatusIndicator ("STOPPED"/"RUNNING" label) - redundant
- Removed Pause/Resume buttons - just Start/Stop toggle now
- Start button shows flame icon with fiery gradient when YOLO mode enabled
## Code Review Fixes
- Added focus trap to SettingsModal for accessibility
- Fixed YOLO button color contrast (WCAG AA compliance)
- Added model validation to AgentStartRequest schema
- Added model to AgentStatus response
- Added aria-labels to all icon-only buttons
- Added role="radiogroup" to model selection
- Added loading indicator during settings save
- Added SQLite timeout (30s) and retry logic with exponential backoff
- Added thread-safe database engine initialization
- Added orphaned lock file cleanup on server startup
## Files Changed
- registry.py: Model config, Settings CRUD, SQLite improvements
- server/routers/settings.py: New settings API
- server/schemas.py: Settings schemas with validation
- server/services/process_manager.py: Model param, orphan cleanup
- ui/src/components/SettingsModal.tsx: New modal component
- ui/src/components/AgentControl.tsx: Simplified to Start/Stop only
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,7 @@ class AgentProcessManager:
|
||||
self.started_at: datetime | None = None
|
||||
self._output_task: asyncio.Task | None = None
|
||||
self.yolo_mode: bool = False # YOLO mode for rapid prototyping
|
||||
self.model: str | None = None # Model being used
|
||||
|
||||
# Support multiple callbacks (for multiple WebSocket clients)
|
||||
self._output_callbacks: Set[Callable[[str], Awaitable[None]]] = set()
|
||||
@@ -214,12 +215,13 @@ class AgentProcessManager:
|
||||
self.status = "stopped"
|
||||
self._remove_lock()
|
||||
|
||||
async def start(self, yolo_mode: bool = False) -> tuple[bool, str]:
|
||||
async def start(self, yolo_mode: bool = False, model: str | None = None) -> tuple[bool, str]:
|
||||
"""
|
||||
Start the agent as a subprocess.
|
||||
|
||||
Args:
|
||||
yolo_mode: If True, run in YOLO mode (no browser testing)
|
||||
model: Model to use (e.g., claude-opus-4-5-20251101)
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
@@ -230,8 +232,9 @@ class AgentProcessManager:
|
||||
if not self._check_lock():
|
||||
return False, "Another agent instance is already running for this project"
|
||||
|
||||
# Store YOLO mode for status queries
|
||||
# Store for status queries
|
||||
self.yolo_mode = yolo_mode
|
||||
self.model = model
|
||||
|
||||
# Build command - pass absolute path to project directory
|
||||
cmd = [
|
||||
@@ -241,6 +244,10 @@ class AgentProcessManager:
|
||||
str(self.project_dir.resolve()),
|
||||
]
|
||||
|
||||
# Add --model flag if model is specified
|
||||
if model:
|
||||
cmd.extend(["--model", model])
|
||||
|
||||
# Add --yolo flag if YOLO mode is enabled
|
||||
if yolo_mode:
|
||||
cmd.append("--yolo")
|
||||
@@ -306,6 +313,7 @@ class AgentProcessManager:
|
||||
self.process = None
|
||||
self.started_at = None
|
||||
self.yolo_mode = False # Reset YOLO mode
|
||||
self.model = None # Reset model
|
||||
|
||||
return True, "Agent stopped"
|
||||
except Exception as e:
|
||||
@@ -387,6 +395,7 @@ class AgentProcessManager:
|
||||
"pid": self.pid,
|
||||
"started_at": self.started_at.isoformat() if self.started_at else None,
|
||||
"yolo_mode": self.yolo_mode,
|
||||
"model": self.model,
|
||||
}
|
||||
|
||||
|
||||
@@ -423,3 +432,73 @@ async def cleanup_all_managers() -> None:
|
||||
|
||||
with _managers_lock:
|
||||
_managers.clear()
|
||||
|
||||
|
||||
def cleanup_orphaned_locks() -> int:
|
||||
"""
|
||||
Clean up orphaned lock files from previous server runs.
|
||||
|
||||
Scans all registered projects for .agent.lock files and removes them
|
||||
if the referenced process is no longer running.
|
||||
|
||||
Returns:
|
||||
Number of orphaned lock files cleaned up
|
||||
"""
|
||||
import sys
|
||||
root = Path(__file__).parent.parent.parent
|
||||
if str(root) not in sys.path:
|
||||
sys.path.insert(0, str(root))
|
||||
|
||||
from registry import list_registered_projects
|
||||
|
||||
cleaned = 0
|
||||
try:
|
||||
projects = list_registered_projects()
|
||||
for name, info in projects.items():
|
||||
project_path = Path(info.get("path", ""))
|
||||
if not project_path.exists():
|
||||
continue
|
||||
|
||||
lock_file = project_path / ".agent.lock"
|
||||
if not lock_file.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
pid_str = lock_file.read_text().strip()
|
||||
pid = int(pid_str)
|
||||
|
||||
# Check if process is still running
|
||||
if psutil.pid_exists(pid):
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
cmdline = " ".join(proc.cmdline())
|
||||
if "autonomous_agent_demo.py" in cmdline:
|
||||
# Process is still running, don't remove
|
||||
logger.info(
|
||||
"Found running agent for project '%s' (PID %d)",
|
||||
name, pid
|
||||
)
|
||||
continue
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
|
||||
# Process not running or not our agent - remove stale lock
|
||||
lock_file.unlink(missing_ok=True)
|
||||
cleaned += 1
|
||||
logger.info("Removed orphaned lock file for project '%s'", name)
|
||||
|
||||
except (ValueError, OSError) as e:
|
||||
# Invalid lock file content - remove it
|
||||
logger.warning(
|
||||
"Removing invalid lock file for project '%s': %s", name, e
|
||||
)
|
||||
lock_file.unlink(missing_ok=True)
|
||||
cleaned += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error during orphan cleanup: %s", e)
|
||||
|
||||
if cleaned:
|
||||
logger.info("Cleaned up %d orphaned lock file(s)", cleaned)
|
||||
|
||||
return cleaned
|
||||
|
||||
Reference in New Issue
Block a user