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:
Auto
2026-01-29 09:08:17 +02:00
parent a12e4aa3b8
commit f6ddffa6e2
9 changed files with 243 additions and 21 deletions

View File

@@ -16,7 +16,7 @@ from datetime import datetime
from pathlib import Path
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.orm import sessionmaker
@@ -85,6 +85,7 @@ class Project(Base):
name = Column(String(50), primary_key=True, index=True)
path = Column(String, nullable=False) # POSIX format for cross-platform
created_at = Column(DateTime, nullable=False)
default_concurrency = Column(Integer, nullable=False, default=3)
class Settings(Base):
@@ -146,12 +147,26 @@ def _get_engine():
}
)
Base.metadata.create_all(bind=_engine)
_migrate_add_default_concurrency(_engine)
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
logger.debug("Initialized registry database at: %s", db_path)
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
def _get_session():
"""
@@ -307,7 +322,8 @@ def list_registered_projects() -> dict[str, dict[str, Any]]:
return {
p.name: {
"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
}
@@ -333,7 +349,8 @@ def get_project_info(name: str) -> dict[str, Any] | None:
return None
return {
"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:
session.close()
@@ -362,6 +379,55 @@ def update_project_path(name: str, new_path: Path) -> bool:
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
# =============================================================================