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

@@ -268,6 +268,7 @@ function App() {
<AgentControl
projectName={selectedProject}
status={wsState.agentStatus}
defaultConcurrency={selectedProjectData?.default_concurrency}
/>
<DevServerControl

View File

@@ -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 {
useStartAgent,
useStopAgent,
useSettings,
useUpdateProjectSettings,
} from '../hooks/useProjects'
import { useNextScheduledRun } from '../hooks/useSchedules'
import { formatNextRun, formatEndTime } from '../lib/timeUtils'
@@ -15,14 +16,47 @@ import { Badge } from '@/components/ui/badge'
interface AgentControlProps {
projectName: string
status: AgentStatus
defaultConcurrency?: number
}
export function AgentControl({ projectName, status }: AgentControlProps) {
export function AgentControl({ projectName, status, defaultConcurrency = 3 }: AgentControlProps) {
const { data: settings } = useSettings()
const yoloMode = settings?.yolo_mode ?? false
// 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 stopAgent = useStopAgent(projectName)
@@ -57,7 +91,7 @@ export function AgentControl({ projectName, status }: AgentControlProps) {
min={1}
max={5}
value={concurrency}
onChange={(e) => setConcurrency(Number(e.target.value))}
onChange={(e) => handleConcurrencyChange(Number(e.target.value))}
disabled={isLoading}
className="w-16 h-2 accent-primary cursor-pointer"
title={`${concurrency} concurrent agent${concurrency > 1 ? 's' : ''}`}

View File

@@ -4,7 +4,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
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
@@ -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
// ============================================================================

View File

@@ -6,6 +6,7 @@ import type {
ProjectSummary,
ProjectDetail,
ProjectPrompts,
ProjectSettingsUpdate,
FeatureListResponse,
Feature,
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
// ============================================================================

View File

@@ -15,6 +15,7 @@ export interface ProjectSummary {
path: string
has_spec: boolean
stats: ProjectStats
default_concurrency: number
}
export interface ProjectDetail extends ProjectSummary {
@@ -536,6 +537,10 @@ export interface SettingsUpdate {
testing_agent_ratio?: number
}
export interface ProjectSettingsUpdate {
default_concurrency?: number
}
// ============================================================================
// Schedule Types
// ============================================================================