mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-31 06:42:06 +00:00
feat: Add robust fallback mechanisms for spec creation chat
Add multiple escape hatches to prevent users from getting stuck during
spec creation when the WebSocket completion signal fails.
Changes:
- Add "Exit to Project" button always visible in chat header
- Add /exit command detection to immediately exit to project
- Add backend GET /api/spec/status/{project} endpoint to poll status file
- Add getSpecStatus() API function in frontend
- Add status file polling (every 3s) in useSpecChat hook
- Update create-spec.md with status file write instructions
How it works:
1. Happy path: Claude writes .spec_status.json as final step, UI polls
and detects completion, shows "Continue to Project" button
2. Escape hatch: User can always click "Exit to Project" or type /exit
to instantly select the project and close modal, then manually start
the agent from the main UI
This ensures users always have a way forward even if the WebSocket
completion detection fails due to tool call tracking issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import type { ChatMessage, ImageAttachment, SpecChatServerMessage, SpecQuestion } from '../lib/types'
|
||||
import { getSpecStatus } from '../lib/api'
|
||||
|
||||
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
|
||||
|
||||
@@ -70,6 +71,66 @@ export function useSpecChat({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Poll status file as fallback completion detection
|
||||
// Claude writes .spec_status.json when done with all spec work
|
||||
useEffect(() => {
|
||||
// Don't poll if already complete
|
||||
if (isComplete) return
|
||||
|
||||
// Start polling after initial delay (let WebSocket try first)
|
||||
const startDelay = setTimeout(() => {
|
||||
const pollInterval = setInterval(async () => {
|
||||
// Stop if already complete
|
||||
if (isCompleteRef.current) {
|
||||
clearInterval(pollInterval)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await getSpecStatus(projectName)
|
||||
|
||||
if (status.exists && status.status === 'complete') {
|
||||
// Status file indicates completion - set complete state
|
||||
setIsComplete(true)
|
||||
setIsLoading(false)
|
||||
|
||||
// Mark any streaming message as done
|
||||
setMessages((prev) => {
|
||||
const lastMessage = prev[prev.length - 1]
|
||||
if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
|
||||
return [
|
||||
...prev.slice(0, -1),
|
||||
{ ...lastMessage, isStreaming: false },
|
||||
]
|
||||
}
|
||||
return prev
|
||||
})
|
||||
|
||||
// Add system message about completion
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: generateId(),
|
||||
role: 'system',
|
||||
content: `Spec creation complete! Files written: ${status.files_written.join(', ')}${status.feature_count ? ` (${status.feature_count} features)` : ''}`,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
])
|
||||
|
||||
clearInterval(pollInterval)
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore polling errors - WebSocket is primary mechanism
|
||||
}
|
||||
}, 3000) // Poll every 3 seconds
|
||||
|
||||
// Cleanup interval on unmount
|
||||
return () => clearInterval(pollInterval)
|
||||
}, 3000) // Start polling after 3 second delay
|
||||
|
||||
return () => clearTimeout(startDelay)
|
||||
}, [projectName, isComplete])
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user