mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-29 22:02:05 +00:00
feat: persist concurrent agents slider at project level
Add `default_concurrency` column to the projects table in the registry
database, allowing each project to remember its preferred concurrency
setting (1-5 agents). The value persists across page refreshes and
app restarts.
Backend changes:
- Add `default_concurrency` column to Project model in registry.py
- Add database migration for existing databases (ALTER TABLE)
- Add get/set_project_concurrency() CRUD functions
- Add ProjectSettingsUpdate schema with validation
- Add PATCH /{name}/settings endpoint in projects router
- Include default_concurrency in ProjectSummary/ProjectDetail responses
Frontend changes:
- Add default_concurrency to ProjectSummary TypeScript interface
- Add ProjectSettingsUpdate type and updateProjectSettings API function
- Add useUpdateProjectSettings React Query mutation hook
- Update AgentControl to accept defaultConcurrency prop
- Sync local state when project changes via useEffect
- Debounce slider changes (500ms) before saving to backend
- Pass defaultConcurrency from selectedProjectData in App.tsx
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,18 +12,43 @@ Pull request(s): $ARGUMENTS
|
|||||||
1. **Retrieve PR Details**
|
1. **Retrieve PR Details**
|
||||||
- Use the GH CLI tool to retrieve the details (descriptions, diffs, comments, feedback, reviews, etc)
|
- Use the GH CLI tool to retrieve the details (descriptions, diffs, comments, feedback, reviews, etc)
|
||||||
|
|
||||||
2. **Analyze Codebase Impact**
|
2. **Assess PR Complexity**
|
||||||
- Use 3 deepdive subagents to analyze the impact on the codebase
|
|
||||||
|
|
||||||
3. **Vision Alignment Check**
|
After retrieving PR details, assess complexity based on:
|
||||||
|
- Number of files changed
|
||||||
|
- Lines added/removed
|
||||||
|
- Number of contributors/commits
|
||||||
|
- Whether changes touch core/architectural files
|
||||||
|
|
||||||
|
### Complexity Tiers
|
||||||
|
|
||||||
|
**Simple** (no deep dive agents needed):
|
||||||
|
- ≤5 files changed AND ≤100 lines changed AND single author
|
||||||
|
- Review directly without spawning agents
|
||||||
|
|
||||||
|
**Medium** (1-2 deep dive agents):
|
||||||
|
- 6-15 files changed, OR 100-500 lines, OR 2 contributors
|
||||||
|
- Spawn 1 agent for focused areas, 2 if changes span multiple domains
|
||||||
|
|
||||||
|
**Complex** (up to 3 deep dive agents):
|
||||||
|
- >15 files, OR >500 lines, OR >2 contributors, OR touches core architecture
|
||||||
|
- Spawn up to 3 agents to analyze different aspects (e.g., security, performance, architecture)
|
||||||
|
|
||||||
|
3. **Analyze Codebase Impact**
|
||||||
|
- Based on the complexity tier determined above, spawn the appropriate number of deep dive subagents
|
||||||
|
- For Simple PRs: analyze directly without spawning agents
|
||||||
|
- For Medium PRs: spawn 1-2 agents focusing on the most impacted areas
|
||||||
|
- For Complex PRs: spawn up to 3 agents to cover security, performance, and architectural concerns
|
||||||
|
|
||||||
|
4. **Vision Alignment Check**
|
||||||
- Read the project's README.md and CLAUDE.md to understand the application's core purpose
|
- Read the project's README.md and CLAUDE.md to understand the application's core purpose
|
||||||
- Assess whether this PR aligns with the application's intended functionality
|
- Assess whether this PR aligns with the application's intended functionality
|
||||||
- If the changes deviate significantly from the core vision or add functionality that doesn't serve the application's purpose, note this in the review
|
- If the changes deviate significantly from the core vision or add functionality that doesn't serve the application's purpose, note this in the review
|
||||||
- This is not a blocker, but should be flagged for the reviewer's consideration
|
- This is not a blocker, but should be flagged for the reviewer's consideration
|
||||||
|
|
||||||
4. **Safety Assessment**
|
5. **Safety Assessment**
|
||||||
- Provide a review on whether the PR is safe to merge as-is
|
- Provide a review on whether the PR is safe to merge as-is
|
||||||
- Provide any feedback in terms of risk level
|
- Provide any feedback in terms of risk level
|
||||||
|
|
||||||
5. **Improvements**
|
6. **Improvements**
|
||||||
- Propose any improvements in terms of importance and complexity
|
- Propose any improvements in terms of importance and complexity
|
||||||
72
registry.py
72
registry.py
@@ -16,7 +16,7 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime, String, create_engine
|
from sqlalchemy import Column, DateTime, Integer, String, create_engine, text
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
@@ -85,6 +85,7 @@ class Project(Base):
|
|||||||
name = Column(String(50), primary_key=True, index=True)
|
name = Column(String(50), primary_key=True, index=True)
|
||||||
path = Column(String, nullable=False) # POSIX format for cross-platform
|
path = Column(String, nullable=False) # POSIX format for cross-platform
|
||||||
created_at = Column(DateTime, nullable=False)
|
created_at = Column(DateTime, nullable=False)
|
||||||
|
default_concurrency = Column(Integer, nullable=False, default=3)
|
||||||
|
|
||||||
|
|
||||||
class Settings(Base):
|
class Settings(Base):
|
||||||
@@ -146,12 +147,26 @@ def _get_engine():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
Base.metadata.create_all(bind=_engine)
|
Base.metadata.create_all(bind=_engine)
|
||||||
|
_migrate_add_default_concurrency(_engine)
|
||||||
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
|
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
|
||||||
logger.debug("Initialized registry database at: %s", db_path)
|
logger.debug("Initialized registry database at: %s", db_path)
|
||||||
|
|
||||||
return _engine, _SessionLocal
|
return _engine, _SessionLocal
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_add_default_concurrency(engine) -> None:
|
||||||
|
"""Add default_concurrency column if missing (for existing databases)."""
|
||||||
|
with engine.connect() as conn:
|
||||||
|
result = conn.execute(text("PRAGMA table_info(projects)"))
|
||||||
|
columns = [row[1] for row in result.fetchall()]
|
||||||
|
if "default_concurrency" not in columns:
|
||||||
|
conn.execute(text(
|
||||||
|
"ALTER TABLE projects ADD COLUMN default_concurrency INTEGER DEFAULT 3"
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
logger.info("Migrated projects table: added default_concurrency column")
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _get_session():
|
def _get_session():
|
||||||
"""
|
"""
|
||||||
@@ -307,7 +322,8 @@ def list_registered_projects() -> dict[str, dict[str, Any]]:
|
|||||||
return {
|
return {
|
||||||
p.name: {
|
p.name: {
|
||||||
"path": p.path,
|
"path": p.path,
|
||||||
"created_at": p.created_at.isoformat() if p.created_at else None
|
"created_at": p.created_at.isoformat() if p.created_at else None,
|
||||||
|
"default_concurrency": getattr(p, 'default_concurrency', 3) or 3
|
||||||
}
|
}
|
||||||
for p in projects
|
for p in projects
|
||||||
}
|
}
|
||||||
@@ -333,7 +349,8 @@ def get_project_info(name: str) -> dict[str, Any] | None:
|
|||||||
return None
|
return None
|
||||||
return {
|
return {
|
||||||
"path": project.path,
|
"path": project.path,
|
||||||
"created_at": project.created_at.isoformat() if project.created_at else None
|
"created_at": project.created_at.isoformat() if project.created_at else None,
|
||||||
|
"default_concurrency": getattr(project, 'default_concurrency', 3) or 3
|
||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
@@ -362,6 +379,55 @@ def update_project_path(name: str, new_path: Path) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_concurrency(name: str) -> int:
|
||||||
|
"""
|
||||||
|
Get project's default concurrency (1-5).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The project name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The default concurrency value (defaults to 3 if not set or project not found).
|
||||||
|
"""
|
||||||
|
_, SessionLocal = _get_engine()
|
||||||
|
session = SessionLocal()
|
||||||
|
try:
|
||||||
|
project = session.query(Project).filter(Project.name == name).first()
|
||||||
|
if project is None:
|
||||||
|
return 3
|
||||||
|
return getattr(project, 'default_concurrency', 3) or 3
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def set_project_concurrency(name: str, concurrency: int) -> bool:
|
||||||
|
"""
|
||||||
|
Set project's default concurrency (1-5).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The project name.
|
||||||
|
concurrency: The concurrency value (1-5).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if updated, False if project wasn't found.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If concurrency is not between 1 and 5.
|
||||||
|
"""
|
||||||
|
if concurrency < 1 or concurrency > 5:
|
||||||
|
raise ValueError("concurrency must be between 1 and 5")
|
||||||
|
|
||||||
|
with _get_session() as session:
|
||||||
|
project = session.query(Project).filter(Project.name == name).first()
|
||||||
|
if not project:
|
||||||
|
return False
|
||||||
|
|
||||||
|
project.default_concurrency = concurrency
|
||||||
|
|
||||||
|
logger.info("Set project '%s' default_concurrency to %d", name, concurrency)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Validation Functions
|
# Validation Functions
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from ..schemas import (
|
|||||||
ProjectDetail,
|
ProjectDetail,
|
||||||
ProjectPrompts,
|
ProjectPrompts,
|
||||||
ProjectPromptsUpdate,
|
ProjectPromptsUpdate,
|
||||||
|
ProjectSettingsUpdate,
|
||||||
ProjectStats,
|
ProjectStats,
|
||||||
ProjectSummary,
|
ProjectSummary,
|
||||||
)
|
)
|
||||||
@@ -63,13 +64,23 @@ def _get_registry_functions():
|
|||||||
sys.path.insert(0, str(root))
|
sys.path.insert(0, str(root))
|
||||||
|
|
||||||
from registry import (
|
from registry import (
|
||||||
|
get_project_concurrency,
|
||||||
get_project_path,
|
get_project_path,
|
||||||
list_registered_projects,
|
list_registered_projects,
|
||||||
register_project,
|
register_project,
|
||||||
|
set_project_concurrency,
|
||||||
unregister_project,
|
unregister_project,
|
||||||
validate_project_path,
|
validate_project_path,
|
||||||
)
|
)
|
||||||
return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path
|
return (
|
||||||
|
register_project,
|
||||||
|
unregister_project,
|
||||||
|
get_project_path,
|
||||||
|
list_registered_projects,
|
||||||
|
validate_project_path,
|
||||||
|
get_project_concurrency,
|
||||||
|
set_project_concurrency,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/projects", tags=["projects"])
|
router = APIRouter(prefix="/api/projects", tags=["projects"])
|
||||||
@@ -102,7 +113,8 @@ def get_project_stats(project_dir: Path) -> ProjectStats:
|
|||||||
async def list_projects():
|
async def list_projects():
|
||||||
"""List all registered projects."""
|
"""List all registered projects."""
|
||||||
_init_imports()
|
_init_imports()
|
||||||
_, _, _, list_registered_projects, validate_project_path = _get_registry_functions()
|
(_, _, _, list_registered_projects, validate_project_path,
|
||||||
|
get_project_concurrency, _) = _get_registry_functions()
|
||||||
|
|
||||||
projects = list_registered_projects()
|
projects = list_registered_projects()
|
||||||
result = []
|
result = []
|
||||||
@@ -123,6 +135,7 @@ async def list_projects():
|
|||||||
path=info["path"],
|
path=info["path"],
|
||||||
has_spec=has_spec,
|
has_spec=has_spec,
|
||||||
stats=stats,
|
stats=stats,
|
||||||
|
default_concurrency=info.get("default_concurrency", 3),
|
||||||
))
|
))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -132,7 +145,8 @@ async def list_projects():
|
|||||||
async def create_project(project: ProjectCreate):
|
async def create_project(project: ProjectCreate):
|
||||||
"""Create a new project at the specified path."""
|
"""Create a new project at the specified path."""
|
||||||
_init_imports()
|
_init_imports()
|
||||||
register_project, _, get_project_path, list_registered_projects, _ = _get_registry_functions()
|
(register_project, _, get_project_path, list_registered_projects,
|
||||||
|
_, _, _) = _get_registry_functions()
|
||||||
|
|
||||||
name = validate_project_name(project.name)
|
name = validate_project_name(project.name)
|
||||||
project_path = Path(project.path).resolve()
|
project_path = Path(project.path).resolve()
|
||||||
@@ -203,6 +217,7 @@ async def create_project(project: ProjectCreate):
|
|||||||
path=project_path.as_posix(),
|
path=project_path.as_posix(),
|
||||||
has_spec=False, # Just created, no spec yet
|
has_spec=False, # Just created, no spec yet
|
||||||
stats=ProjectStats(passing=0, total=0, percentage=0.0),
|
stats=ProjectStats(passing=0, total=0, percentage=0.0),
|
||||||
|
default_concurrency=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -210,7 +225,7 @@ async def create_project(project: ProjectCreate):
|
|||||||
async def get_project(name: str):
|
async def get_project(name: str):
|
||||||
"""Get detailed information about a project."""
|
"""Get detailed information about a project."""
|
||||||
_init_imports()
|
_init_imports()
|
||||||
_, _, get_project_path, _, _ = _get_registry_functions()
|
(_, _, get_project_path, _, _, get_project_concurrency, _) = _get_registry_functions()
|
||||||
|
|
||||||
name = validate_project_name(name)
|
name = validate_project_name(name)
|
||||||
project_dir = get_project_path(name)
|
project_dir = get_project_path(name)
|
||||||
@@ -231,6 +246,7 @@ async def get_project(name: str):
|
|||||||
has_spec=has_spec,
|
has_spec=has_spec,
|
||||||
stats=stats,
|
stats=stats,
|
||||||
prompts_dir=str(prompts_dir),
|
prompts_dir=str(prompts_dir),
|
||||||
|
default_concurrency=get_project_concurrency(name),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -244,7 +260,7 @@ async def delete_project(name: str, delete_files: bool = False):
|
|||||||
delete_files: If True, also delete the project directory and files
|
delete_files: If True, also delete the project directory and files
|
||||||
"""
|
"""
|
||||||
_init_imports()
|
_init_imports()
|
||||||
_, unregister_project, get_project_path, _, _ = _get_registry_functions()
|
(_, unregister_project, get_project_path, _, _, _, _) = _get_registry_functions()
|
||||||
|
|
||||||
name = validate_project_name(name)
|
name = validate_project_name(name)
|
||||||
project_dir = get_project_path(name)
|
project_dir = get_project_path(name)
|
||||||
@@ -280,7 +296,7 @@ async def delete_project(name: str, delete_files: bool = False):
|
|||||||
async def get_project_prompts(name: str):
|
async def get_project_prompts(name: str):
|
||||||
"""Get the content of project prompt files."""
|
"""Get the content of project prompt files."""
|
||||||
_init_imports()
|
_init_imports()
|
||||||
_, _, get_project_path, _, _ = _get_registry_functions()
|
(_, _, get_project_path, _, _, _, _) = _get_registry_functions()
|
||||||
|
|
||||||
name = validate_project_name(name)
|
name = validate_project_name(name)
|
||||||
project_dir = get_project_path(name)
|
project_dir = get_project_path(name)
|
||||||
@@ -313,7 +329,7 @@ async def get_project_prompts(name: str):
|
|||||||
async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
|
async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
|
||||||
"""Update project prompt files."""
|
"""Update project prompt files."""
|
||||||
_init_imports()
|
_init_imports()
|
||||||
_, _, get_project_path, _, _ = _get_registry_functions()
|
(_, _, get_project_path, _, _, _, _) = _get_registry_functions()
|
||||||
|
|
||||||
name = validate_project_name(name)
|
name = validate_project_name(name)
|
||||||
project_dir = get_project_path(name)
|
project_dir = get_project_path(name)
|
||||||
@@ -343,7 +359,7 @@ async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
|
|||||||
async def get_project_stats_endpoint(name: str):
|
async def get_project_stats_endpoint(name: str):
|
||||||
"""Get current progress statistics for a project."""
|
"""Get current progress statistics for a project."""
|
||||||
_init_imports()
|
_init_imports()
|
||||||
_, _, get_project_path, _, _ = _get_registry_functions()
|
(_, _, get_project_path, _, _, _, _) = _get_registry_functions()
|
||||||
|
|
||||||
name = validate_project_name(name)
|
name = validate_project_name(name)
|
||||||
project_dir = get_project_path(name)
|
project_dir = get_project_path(name)
|
||||||
@@ -355,3 +371,40 @@ async def get_project_stats_endpoint(name: str):
|
|||||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||||
|
|
||||||
return get_project_stats(project_dir)
|
return get_project_stats(project_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{name}/settings", response_model=ProjectDetail)
|
||||||
|
async def update_project_settings(name: str, settings: ProjectSettingsUpdate):
|
||||||
|
"""Update project-level settings (concurrency, etc.)."""
|
||||||
|
_init_imports()
|
||||||
|
(_, _, get_project_path, _, _, get_project_concurrency,
|
||||||
|
set_project_concurrency) = _get_registry_functions()
|
||||||
|
|
||||||
|
name = validate_project_name(name)
|
||||||
|
project_dir = get_project_path(name)
|
||||||
|
|
||||||
|
if not project_dir:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
|
||||||
|
|
||||||
|
if not project_dir.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||||
|
|
||||||
|
# Update concurrency if provided
|
||||||
|
if settings.default_concurrency is not None:
|
||||||
|
success = set_project_concurrency(name, settings.default_concurrency)
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to update concurrency")
|
||||||
|
|
||||||
|
# Return updated project details
|
||||||
|
has_spec = _check_spec_exists(project_dir)
|
||||||
|
stats = get_project_stats(project_dir)
|
||||||
|
prompts_dir = _get_project_prompts_dir(project_dir)
|
||||||
|
|
||||||
|
return ProjectDetail(
|
||||||
|
name=name,
|
||||||
|
path=project_dir.as_posix(),
|
||||||
|
has_spec=has_spec,
|
||||||
|
stats=stats,
|
||||||
|
prompts_dir=str(prompts_dir),
|
||||||
|
default_concurrency=get_project_concurrency(name),
|
||||||
|
)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class ProjectSummary(BaseModel):
|
|||||||
path: str
|
path: str
|
||||||
has_spec: bool
|
has_spec: bool
|
||||||
stats: ProjectStats
|
stats: ProjectStats
|
||||||
|
default_concurrency: int = 3
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetail(BaseModel):
|
class ProjectDetail(BaseModel):
|
||||||
@@ -54,6 +55,7 @@ class ProjectDetail(BaseModel):
|
|||||||
has_spec: bool
|
has_spec: bool
|
||||||
stats: ProjectStats
|
stats: ProjectStats
|
||||||
prompts_dir: str
|
prompts_dir: str
|
||||||
|
default_concurrency: int = 3
|
||||||
|
|
||||||
|
|
||||||
class ProjectPrompts(BaseModel):
|
class ProjectPrompts(BaseModel):
|
||||||
@@ -70,6 +72,18 @@ class ProjectPromptsUpdate(BaseModel):
|
|||||||
coding_prompt: str | None = None
|
coding_prompt: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectSettingsUpdate(BaseModel):
|
||||||
|
"""Request schema for updating project-level settings."""
|
||||||
|
default_concurrency: int | None = None
|
||||||
|
|
||||||
|
@field_validator('default_concurrency')
|
||||||
|
@classmethod
|
||||||
|
def validate_concurrency(cls, v: int | None) -> int | None:
|
||||||
|
if v is not None and (v < 1 or v > 5):
|
||||||
|
raise ValueError("default_concurrency must be between 1 and 5")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Feature Schemas
|
# Feature Schemas
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ function App() {
|
|||||||
<AgentControl
|
<AgentControl
|
||||||
projectName={selectedProject}
|
projectName={selectedProject}
|
||||||
status={wsState.agentStatus}
|
status={wsState.agentStatus}
|
||||||
|
defaultConcurrency={selectedProjectData?.default_concurrency}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DevServerControl
|
<DevServerControl
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { Play, Square, Loader2, GitBranch, Clock } from 'lucide-react'
|
import { Play, Square, Loader2, GitBranch, Clock } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
useStartAgent,
|
useStartAgent,
|
||||||
useStopAgent,
|
useStopAgent,
|
||||||
useSettings,
|
useSettings,
|
||||||
|
useUpdateProjectSettings,
|
||||||
} from '../hooks/useProjects'
|
} from '../hooks/useProjects'
|
||||||
import { useNextScheduledRun } from '../hooks/useSchedules'
|
import { useNextScheduledRun } from '../hooks/useSchedules'
|
||||||
import { formatNextRun, formatEndTime } from '../lib/timeUtils'
|
import { formatNextRun, formatEndTime } from '../lib/timeUtils'
|
||||||
@@ -15,14 +16,47 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
interface AgentControlProps {
|
interface AgentControlProps {
|
||||||
projectName: string
|
projectName: string
|
||||||
status: AgentStatus
|
status: AgentStatus
|
||||||
|
defaultConcurrency?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentControl({ projectName, status }: AgentControlProps) {
|
export function AgentControl({ projectName, status, defaultConcurrency = 3 }: AgentControlProps) {
|
||||||
const { data: settings } = useSettings()
|
const { data: settings } = useSettings()
|
||||||
const yoloMode = settings?.yolo_mode ?? false
|
const yoloMode = settings?.yolo_mode ?? false
|
||||||
|
|
||||||
// Concurrency: 1 = single agent, 2-5 = parallel
|
// Concurrency: 1 = single agent, 2-5 = parallel
|
||||||
const [concurrency, setConcurrency] = useState(3)
|
const [concurrency, setConcurrency] = useState(defaultConcurrency)
|
||||||
|
|
||||||
|
// Sync concurrency when project changes or defaultConcurrency updates
|
||||||
|
useEffect(() => {
|
||||||
|
setConcurrency(defaultConcurrency)
|
||||||
|
}, [defaultConcurrency])
|
||||||
|
|
||||||
|
// Debounced save for concurrency changes
|
||||||
|
const updateProjectSettings = useUpdateProjectSettings(projectName)
|
||||||
|
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
const handleConcurrencyChange = useCallback((newConcurrency: number) => {
|
||||||
|
setConcurrency(newConcurrency)
|
||||||
|
|
||||||
|
// Clear previous timeout
|
||||||
|
if (saveTimeoutRef.current) {
|
||||||
|
clearTimeout(saveTimeoutRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce save (500ms)
|
||||||
|
saveTimeoutRef.current = setTimeout(() => {
|
||||||
|
updateProjectSettings.mutate({ default_concurrency: newConcurrency })
|
||||||
|
}, 500)
|
||||||
|
}, [updateProjectSettings])
|
||||||
|
|
||||||
|
// Cleanup timeout on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (saveTimeoutRef.current) {
|
||||||
|
clearTimeout(saveTimeoutRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const startAgent = useStartAgent(projectName)
|
const startAgent = useStartAgent(projectName)
|
||||||
const stopAgent = useStopAgent(projectName)
|
const stopAgent = useStopAgent(projectName)
|
||||||
@@ -57,7 +91,7 @@ export function AgentControl({ projectName, status }: AgentControlProps) {
|
|||||||
min={1}
|
min={1}
|
||||||
max={5}
|
max={5}
|
||||||
value={concurrency}
|
value={concurrency}
|
||||||
onChange={(e) => setConcurrency(Number(e.target.value))}
|
onChange={(e) => handleConcurrencyChange(Number(e.target.value))}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-16 h-2 accent-primary cursor-pointer"
|
className="w-16 h-2 accent-primary cursor-pointer"
|
||||||
title={`${concurrency} concurrent agent${concurrency > 1 ? 's' : ''}`}
|
title={`${concurrency} concurrent agent${concurrency > 1 ? 's' : ''}`}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import * as api from '../lib/api'
|
import * as api from '../lib/api'
|
||||||
import type { FeatureCreate, FeatureUpdate, ModelsResponse, Settings, SettingsUpdate } from '../lib/types'
|
import type { FeatureCreate, FeatureUpdate, ModelsResponse, ProjectSettingsUpdate, Settings, SettingsUpdate } from '../lib/types'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Projects
|
// Projects
|
||||||
@@ -48,6 +48,19 @@ export function useDeleteProject() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useUpdateProjectSettings(projectName: string) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (settings: ProjectSettingsUpdate) =>
|
||||||
|
api.updateProjectSettings(projectName, settings),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['projects'] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['project', projectName] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Features
|
// Features
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
ProjectSummary,
|
ProjectSummary,
|
||||||
ProjectDetail,
|
ProjectDetail,
|
||||||
ProjectPrompts,
|
ProjectPrompts,
|
||||||
|
ProjectSettingsUpdate,
|
||||||
FeatureListResponse,
|
FeatureListResponse,
|
||||||
Feature,
|
Feature,
|
||||||
FeatureCreate,
|
FeatureCreate,
|
||||||
@@ -100,6 +101,16 @@ export async function updateProjectPrompts(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateProjectSettings(
|
||||||
|
name: string,
|
||||||
|
settings: ProjectSettingsUpdate
|
||||||
|
): Promise<ProjectDetail> {
|
||||||
|
return fetchJSON(`/projects/${encodeURIComponent(name)}/settings`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(settings),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Features API
|
// Features API
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface ProjectSummary {
|
|||||||
path: string
|
path: string
|
||||||
has_spec: boolean
|
has_spec: boolean
|
||||||
stats: ProjectStats
|
stats: ProjectStats
|
||||||
|
default_concurrency: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectDetail extends ProjectSummary {
|
export interface ProjectDetail extends ProjectSummary {
|
||||||
@@ -536,6 +537,10 @@ export interface SettingsUpdate {
|
|||||||
testing_agent_ratio?: number
|
testing_agent_ratio?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectSettingsUpdate {
|
||||||
|
default_concurrency?: number
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Schedule Types
|
// Schedule Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user