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:
Auto
2026-01-02 10:54:42 +02:00
parent 74d3bd9d8b
commit e8f3b99a42
6 changed files with 213 additions and 4 deletions

View File

@@ -148,6 +148,12 @@ export function NewProjectModal({
setSpecMethod(null)
}
const handleExitToProject = () => {
// Exit chat and go directly to project - user can start agent manually
onProjectCreated(projectName.trim())
handleClose()
}
const handleClose = () => {
setStep('name')
setProjectName('')
@@ -178,6 +184,7 @@ export function NewProjectModal({
projectName={projectName.trim()}
onComplete={handleSpecComplete}
onCancel={handleChatCancel}
onExitToProject={handleExitToProject}
initializerStatus={initializerStatus}
initializerError={initializerError}
onRetryInitializer={handleRetryInitializer}

View File

@@ -6,7 +6,7 @@
*/
import { useCallback, useEffect, useRef, useState } from 'react'
import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip } from 'lucide-react'
import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip, ExternalLink } from 'lucide-react'
import { useSpecChat } from '../hooks/useSpecChat'
import { ChatMessage } from './ChatMessage'
import { QuestionOptions } from './QuestionOptions'
@@ -23,6 +23,7 @@ interface SpecCreationChatProps {
projectName: string
onComplete: (specPath: string, yoloMode?: boolean) => void
onCancel: () => void
onExitToProject: () => void // Exit to project without starting agent
initializerStatus?: InitializerStatus
initializerError?: string | null
onRetryInitializer?: () => void
@@ -32,6 +33,7 @@ export function SpecCreationChat({
projectName,
onComplete,
onCancel,
onExitToProject,
initializerStatus = 'idle',
initializerError = null,
onRetryInitializer,
@@ -86,6 +88,13 @@ export function SpecCreationChat({
// Allow sending if there's text OR attachments
if ((!trimmed && pendingAttachments.length === 0) || isLoading) return
// Detect /exit command - exit to project without sending to Claude
if (/^\s*\/exit\s*$/i.test(trimmed)) {
setInput('')
onExitToProject()
return
}
sendMessage(trimmed, pendingAttachments.length > 0 ? pendingAttachments : undefined)
setInput('')
setPendingAttachments([]) // Clear attachments after sending
@@ -210,6 +219,16 @@ export function SpecCreationChat({
</span>
)}
{/* Exit to Project - always visible escape hatch */}
<button
onClick={onExitToProject}
className="neo-btn neo-btn-ghost text-sm py-2"
title="Exit chat and go to project (you can start the agent manually)"
>
<ExternalLink size={16} />
Exit to Project
</button>
<button
onClick={onCancel}
className="neo-btn neo-btn-ghost p-2"
@@ -347,7 +366,7 @@ export function SpecCreationChat({
? 'Or type a custom response...'
: pendingAttachments.length > 0
? 'Add a message with your image(s)...'
: 'Type your response...'
: 'Type your response... (or /exit to go to project)'
}
className="neo-input flex-1"
disabled={(isLoading && !currentQuestions) || connectionStatus !== 'connected'}