mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 14:22:04 +00:00
Implements feature locking to prevent multiple agent sessions from working on the same feature simultaneously. This is essential for parallel agent execution. Database changes: - Add `in_progress` boolean column to Feature model - Add migration function to handle existing databases MCP Server tools: - Add `feature_mark_in_progress` - lock feature when starting work - Add `feature_clear_in_progress` - unlock feature when abandoning - Update `feature_get_next` to skip in-progress features - Update `feature_get_stats` to include in_progress count - Update `feature_mark_passing` and `feature_skip` to clear in_progress Backend updates: - Update progress.py to track and display in_progress count - Update features router to properly categorize in-progress features - Update WebSocket to broadcast in_progress in progress updates - Add in_progress to FeatureResponse schema Frontend updates: - Add in_progress to TypeScript types (Feature, ProjectStats, WSProgressMessage) - Update useWebSocket hook to track in_progress state Prompt template: - Add instructions for agents to mark features in-progress immediately - Document new MCP tools in allowed tools section Also fixes spec_chat_session.py to use absolute project path instead of relative path for consistency with CLI behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
164 lines
4.1 KiB
TypeScript
164 lines
4.1 KiB
TypeScript
/**
|
|
* WebSocket Hook for Real-time Updates
|
|
*/
|
|
|
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
import type { WSMessage, AgentStatus } from '../lib/types'
|
|
|
|
interface WebSocketState {
|
|
progress: {
|
|
passing: number
|
|
in_progress: number
|
|
total: number
|
|
percentage: number
|
|
}
|
|
agentStatus: AgentStatus
|
|
logs: Array<{ line: string; timestamp: string }>
|
|
isConnected: boolean
|
|
}
|
|
|
|
const MAX_LOGS = 100 // Keep last 100 log lines
|
|
|
|
export function useProjectWebSocket(projectName: string | null) {
|
|
const [state, setState] = useState<WebSocketState>({
|
|
progress: { passing: 0, in_progress: 0, total: 0, percentage: 0 },
|
|
agentStatus: 'stopped',
|
|
logs: [],
|
|
isConnected: false,
|
|
})
|
|
|
|
const wsRef = useRef<WebSocket | null>(null)
|
|
const reconnectTimeoutRef = useRef<number | null>(null)
|
|
const reconnectAttempts = useRef(0)
|
|
|
|
const connect = useCallback(() => {
|
|
if (!projectName) return
|
|
|
|
// Build WebSocket URL
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
const host = window.location.host
|
|
const wsUrl = `${protocol}//${host}/ws/projects/${encodeURIComponent(projectName)}`
|
|
|
|
try {
|
|
const ws = new WebSocket(wsUrl)
|
|
wsRef.current = ws
|
|
|
|
ws.onopen = () => {
|
|
setState(prev => ({ ...prev, isConnected: true }))
|
|
reconnectAttempts.current = 0
|
|
}
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const message: WSMessage = JSON.parse(event.data)
|
|
|
|
switch (message.type) {
|
|
case 'progress':
|
|
setState(prev => ({
|
|
...prev,
|
|
progress: {
|
|
passing: message.passing,
|
|
in_progress: message.in_progress,
|
|
total: message.total,
|
|
percentage: message.percentage,
|
|
},
|
|
}))
|
|
break
|
|
|
|
case 'agent_status':
|
|
setState(prev => ({
|
|
...prev,
|
|
agentStatus: message.status,
|
|
}))
|
|
break
|
|
|
|
case 'log':
|
|
setState(prev => ({
|
|
...prev,
|
|
logs: [
|
|
...prev.logs.slice(-MAX_LOGS + 1),
|
|
{ line: message.line, timestamp: message.timestamp },
|
|
],
|
|
}))
|
|
break
|
|
|
|
case 'feature_update':
|
|
// Feature updates will trigger a refetch via React Query
|
|
break
|
|
|
|
case 'pong':
|
|
// Heartbeat response
|
|
break
|
|
}
|
|
} catch {
|
|
console.error('Failed to parse WebSocket message')
|
|
}
|
|
}
|
|
|
|
ws.onclose = () => {
|
|
setState(prev => ({ ...prev, isConnected: false }))
|
|
wsRef.current = null
|
|
|
|
// Exponential backoff reconnection
|
|
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000)
|
|
reconnectAttempts.current++
|
|
|
|
reconnectTimeoutRef.current = window.setTimeout(() => {
|
|
connect()
|
|
}, delay)
|
|
}
|
|
|
|
ws.onerror = () => {
|
|
ws.close()
|
|
}
|
|
} catch {
|
|
// Failed to connect, will retry via onclose
|
|
}
|
|
}, [projectName])
|
|
|
|
// Send ping to keep connection alive
|
|
const sendPing = useCallback(() => {
|
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
wsRef.current.send(JSON.stringify({ type: 'ping' }))
|
|
}
|
|
}, [])
|
|
|
|
// Connect when project changes
|
|
useEffect(() => {
|
|
if (!projectName) {
|
|
// Disconnect if no project
|
|
if (wsRef.current) {
|
|
wsRef.current.close()
|
|
wsRef.current = null
|
|
}
|
|
return
|
|
}
|
|
|
|
connect()
|
|
|
|
// Ping every 30 seconds
|
|
const pingInterval = setInterval(sendPing, 30000)
|
|
|
|
return () => {
|
|
clearInterval(pingInterval)
|
|
if (reconnectTimeoutRef.current) {
|
|
clearTimeout(reconnectTimeoutRef.current)
|
|
}
|
|
if (wsRef.current) {
|
|
wsRef.current.close()
|
|
wsRef.current = null
|
|
}
|
|
}
|
|
}, [projectName, connect, sendPing])
|
|
|
|
// Clear logs function
|
|
const clearLogs = useCallback(() => {
|
|
setState(prev => ({ ...prev, logs: [] }))
|
|
}, [])
|
|
|
|
return {
|
|
...state,
|
|
clearLogs,
|
|
}
|
|
}
|