Files
autocoder/ui/src/hooks/useWebSocket.ts
Auto f180e1933d Add in-progress status tracking for features
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>
2025-12-30 19:00:49 +02:00

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,
}
}