diff --git a/api/database.py b/api/database.py index 662f3b3..e7bcf46 100644 --- a/api/database.py +++ b/api/database.py @@ -81,6 +81,7 @@ class Schedule(Base): # Agent configuration for scheduled runs yolo_mode = Column(Boolean, nullable=False, default=False) model = Column(String(50), nullable=True) # None = use global default + max_concurrency = Column(Integer, nullable=False, default=3) # 1-5 concurrent agents # Crash recovery tracking crash_count = Column(Integer, nullable=False, default=0) # Resets at window start @@ -104,6 +105,7 @@ class Schedule(Base): "enabled": self.enabled, "yolo_mode": self.yolo_mode, "model": self.model, + "max_concurrency": self.max_concurrency, "crash_count": self.crash_count, "created_at": self.created_at.isoformat() if self.created_at else None, } @@ -275,6 +277,14 @@ def _migrate_add_schedules_tables(engine) -> None: ) conn.commit() + # Add max_concurrency column if missing (for upgrades) + if "max_concurrency" not in columns: + with engine.connect() as conn: + conn.execute( + text("ALTER TABLE schedules ADD COLUMN max_concurrency INTEGER DEFAULT 3") + ) + conn.commit() + def create_database(project_dir: Path) -> tuple: """ diff --git a/server/schemas.py b/server/schemas.py index c93e756..451baaf 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -501,6 +501,12 @@ class ScheduleCreate(BaseModel): enabled: bool = True yolo_mode: bool = False model: str | None = None + max_concurrency: int = Field( + default=3, + ge=1, + le=5, + description="Max concurrent agents (1-5)" + ) @field_validator('model') @classmethod @@ -522,6 +528,7 @@ class ScheduleUpdate(BaseModel): enabled: bool | None = None yolo_mode: bool | None = None model: str | None = None + max_concurrency: int | None = Field(None, ge=1, le=5) @field_validator('model') @classmethod @@ -542,6 +549,7 @@ class ScheduleResponse(BaseModel): enabled: bool yolo_mode: bool model: str | None + max_concurrency: int crash_count: int created_at: datetime diff --git a/server/services/scheduler_service.py b/server/services/scheduler_service.py index 239f6cd..d6fc1b6 100644 --- a/server/services/scheduler_service.py +++ b/server/services/scheduler_service.py @@ -368,10 +368,14 @@ class SchedulerService: logger.info(f"Agent already running for {project_name}, skipping scheduled start") return - logger.info(f"Starting agent for {project_name} (schedule {schedule.id}, yolo={schedule.yolo_mode})") + logger.info( + f"Starting agent for {project_name} " + f"(schedule {schedule.id}, yolo={schedule.yolo_mode}, concurrency={schedule.max_concurrency})" + ) success, msg = await manager.start( yolo_mode=schedule.yolo_mode, model=schedule.model, + max_concurrency=schedule.max_concurrency, ) if success: diff --git a/ui/src/components/ScheduleModal.tsx b/ui/src/components/ScheduleModal.tsx index 940ca0b..0d54865 100644 --- a/ui/src/components/ScheduleModal.tsx +++ b/ui/src/components/ScheduleModal.tsx @@ -6,7 +6,7 @@ */ import { useState, useEffect, useRef } from 'react' -import { Clock, Trash2, X } from 'lucide-react' +import { Clock, GitBranch, Trash2, X } from 'lucide-react' import { useSchedules, useCreateSchedule, @@ -47,6 +47,7 @@ export function ScheduleModal({ projectName, isOpen, onClose }: ScheduleModalPro enabled: true, yolo_mode: false, model: null, + max_concurrency: 3, }) const [error, setError] = useState(null) @@ -124,6 +125,7 @@ export function ScheduleModal({ projectName, isOpen, onClose }: ScheduleModalPro enabled: true, yolo_mode: false, model: null, + max_concurrency: 3, }) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create schedule') @@ -242,6 +244,10 @@ export function ScheduleModal({ projectName, isOpen, onClose }: ScheduleModalPro {schedule.yolo_mode && ( ⚡ YOLO mode )} + + + {schedule.max_concurrency}x + {schedule.model && Model: {schedule.model}} {schedule.crash_count > 0 && ( Crashes: {schedule.crash_count} @@ -369,6 +375,37 @@ export function ScheduleModal({ projectName, isOpen, onClose }: ScheduleModalPro + {/* Concurrency slider */} +
+ +
+ 1 ? 'text-[var(--color-neo-primary)]' : 'text-gray-400'} + /> + + setNewSchedule((prev) => ({ ...prev, max_concurrency: Number(e.target.value) })) + } + className="flex-1 h-2 accent-[var(--color-neo-primary)] cursor-pointer" + title={`${newSchedule.max_concurrency} concurrent agent${newSchedule.max_concurrency > 1 ? 's' : ''}`} + aria-label="Set number of concurrent agents" + /> + + {newSchedule.max_concurrency}x + +
+
+ Run {newSchedule.max_concurrency} agent{newSchedule.max_concurrency > 1 ? 's' : ''} in parallel for faster feature completion +
+
+ {/* Model selection (optional) */}