1→/**
     2→ * Spec Creation Chat Component
     3→ *
     4→ * Full chat interface for interactive spec creation with Claude.
     5→ * Handles the 7-phase conversation flow for creating app specifications.
     6→ */
     7→
     8→import { useCallback, useEffect, useRef, useState } from 'react'
     9→import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip, ExternalLink } from 'lucide-react'
    10→import { useSpecChat } from '../hooks/useSpecChat'
    11→import { ChatMessage } from './ChatMessage'
    12→import { QuestionOptions } from './QuestionOptions'
    13→import { TypingIndicator } from './TypingIndicator'
    14→import type { ImageAttachment } from '../lib/types'
    15→import type { CodebaseAnalysis } from '../lib/api'
    16→
    17→// Image upload validation constants
    18→const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
    19→const ALLOWED_TYPES = ['image/jpeg', 'image/png']
    20→
    21→type InitializerStatus = 'idle' | 'starting' | 'error'
    22→
    23→interface SpecCreationChatProps {
    24→  projectName: string
    25→  onComplete: (specPath: string, yoloMode?: boolean) => void
    26→  onCancel: () => void
    27→  onExitToProject: () => void  // Exit to project without starting agent
    28→  initializerStatus?: InitializerStatus
    29→  initializerError?: string | null
    30→  onRetryInitializer?: () => void
    31→  /** If true, use import-spec skill for existing codebases */
    32→  isImport?: boolean
    33→  /** If true, use refine-spec skill to modify existing spec */
    34→  isRefine?: boolean
    35→  /** Codebase analysis results from import */
    36→  codebaseAnalysis?: CodebaseAnalysis | null
    37→}
    38→
    39→// Format codebase analysis for Claude context
    40→function formatAnalysisForClaude(analysis: CodebaseAnalysis): string {
    41→  const lines: string[] = [
    42→    '# EXISTING PROJECT ANALYSIS',
    43→    '',
    44→    `**Project Directory:** ${analysis.project_name || 'Unknown'}`,
    45→    '',
    46→  ]
    47→
    48→  if (Object.keys(analysis.detected_stack).length > 0) {
    49→    lines.push('## Detected Technology Stack')
    50→    for (const [category, tech] of Object.entries(analysis.detected_stack)) {
    51→      lines.push(`- **${category}:** ${tech}`)
    52→    }
    53→    lines.push('')
    54→  }
    55→
    56→  if (analysis.directories.length > 0) {
    57→    lines.push('## Directory Structure')
    58→    lines.push('Top-level directories:')
    59→    for (const dir of analysis.directories.slice(0, 10)) {
    60→      lines.push(`- \`${dir}/\``)
    61→    }
    62→    if (analysis.directories.length > 10) {
    63→      lines.push(`- ... and ${analysis.directories.length - 10} more`)
    64→    }
    65→    lines.push('')
    66→  }
    67→
    68→  if (Object.keys(analysis.file_types).length > 0) {
    69→    lines.push('## File Statistics')
    70→    lines.push(`Total code files: ${analysis.total_files}`)
    71→    lines.push('')
    72→    lines.push('Files by type:')
    73→    for (const [ext, count] of Object.entries(analysis.file_types).slice(0, 8)) {
    74→      lines.push(`- \`${ext}\`: ${count} files`)
    75→    }
    76→    lines.push('')
    77→  }
    78→
    79→  if (analysis.entry_points.length > 0) {
    80→    lines.push('## Entry Points')
    81→    for (const ep of analysis.entry_points.slice(0, 5)) {
    82→      lines.push(`- ${ep}`)
    83→    }
    84→    lines.push('')
    85→  }
    86→
    87→  if (analysis.claude_md_content) {
    88→    lines.push('## Project Documentation (from CLAUDE.md)')
    89→    lines.push('')
    90→    lines.push(analysis.claude_md_content)
    91→    lines.push('')
    92→  } else if (analysis.readme_content) {
    93→    lines.push('## Project Documentation (from README.md)')
    94→    lines.push('')
    95→    lines.push(analysis.readme_content)
    96→    lines.push('')
    97→  }
    98→
    99→  lines.push('---')
   100→  lines.push('')
   101→
   102→  return lines.join('\n')
   103→}
   104→
   105→export function SpecCreationChat({
   106→  projectName,
   107→  onComplete,
   108→  onCancel,
   109→  onExitToProject,
   110→  initializerStatus = 'idle',
   111→  initializerError = null,
   112→  onRetryInitializer,
   113→  isImport = false,
   114→  isRefine = false,
   115→  codebaseAnalysis = null,
   116→}: SpecCreationChatProps) {
   117→  const [input, setInput] = useState('')
   118→  const [error, setError] = useState<string | null>(null)
   119→  const [yoloEnabled, setYoloEnabled] = useState(false)
   120→  const [pendingAttachments, setPendingAttachments] = useState<ImageAttachment[]>([])
   121→  const messagesEndRef = useRef<HTMLDivElement>(null)
   122→  const inputRef = useRef<HTMLInputElement>(null)
   123→  const fileInputRef = useRef<HTMLInputElement>(null)
   124→
   125→  const {
   126→    messages,
   127→    isLoading,
   128→    isComplete,
   129→    connectionStatus,
   130→    currentQuestions,
   131→    start,
   132→    sendMessage,
   133→    sendAnswer,
   134→    disconnect,
   135→  } = useSpecChat({
   136→    projectName,
   137→    onComplete,
   138→    onError: (err) => setError(err),
   139→    isImport,
   140→    codebaseAnalysis: codebaseAnalysis ? formatAnalysisForClaude(codebaseAnalysis) : null,
   141→  })
   142→
   143→  // Start the chat session when component mounts
   144→  useEffect(() => {
   145→    start()
   146→
   147→    return () => {
   148→      disconnect()
   149→    }
   150→  }, []) // eslint-disable-line react-hooks/exhaustive-deps
   151→
   152→  // Scroll to bottom when messages change
   153→  useEffect(() => {
   154→    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
   155→  }, [messages, currentQuestions, isLoading])
   156→
   157→  // Focus input when not loading and no questions
   158→  useEffect(() => {
   159→    if (!isLoading && !currentQuestions && inputRef.current) {
   160→      inputRef.current.focus()
   161→    }
   162→  }, [isLoading, currentQuestions])
   163→
   164→  const handleSendMessage = () => {
   165→    const trimmed = input.trim()
   166→    // Allow sending if there's text OR attachments
   167→    if ((!trimmed && pendingAttachments.length === 0) || isLoading) return
   168→
   169→    // Detect /exit command - exit to project without sending to Claude
   170→    if (/^\s*\/exit\s*$/i.test(trimmed)) {
   171→      setInput('')
   172→      onExitToProject()
   173→      return
   174→    }
   175→
   176→    sendMessage(trimmed, pendingAttachments.length > 0 ? pendingAttachments : undefined)
   177→    setInput('')
   178→    setPendingAttachments([]) // Clear attachments after sending
   179→  }
   180→
   181→  const handleKeyDown = (e: React.KeyboardEvent) => {
   182→    if (e.key === 'Enter' && !e.shiftKey) {
   183→      e.preventDefault()
   184→      handleSendMessage()
   185→    }
   186→  }
   187→
   188→  const handleAnswerSubmit = (answers: Record<string, string | string[]>) => {
   189→    sendAnswer(answers)
   190→  }
   191→
   192→  // File handling for image attachments
   193→  const handleFileSelect = useCallback((files: FileList | null) => {
   194→    if (!files) return
   195→
   196→    Array.from(files).forEach((file) => {
   197→      // Validate file type
   198→      if (!ALLOWED_TYPES.includes(file.type)) {
   199→        setError(`Invalid file type: ${file.name}. Only JPEG and PNG are supported.`)
   200→        return
   201→      }
   202→
   203→      // Validate file size
   204→      if (file.size > MAX_FILE_SIZE) {
   205→        setError(`File too large: ${file.name}. Maximum size is 5 MB.`)
   206→        return
   207→      }
   208→
   209→      // Read and convert to base64
   210→      const reader = new FileReader()
   211→      reader.onload = (e) => {
   212→        const dataUrl = e.target?.result as string
   213→        // dataUrl is "data:image/png;base64,XXXXXX"
   214→        const base64Data = dataUrl.split(',')[1]
   215→
   216→        const attachment: ImageAttachment = {
   217→          id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
   218→          filename: file.name,
   219→          mimeType: file.type as 'image/jpeg' | 'image/png',
   220→          base64Data,
   221→          previewUrl: dataUrl,
   222→          size: file.size,
   223→        }
   224→
   225→        setPendingAttachments((prev) => [...prev, attachment])
   226→      }
   227→      reader.readAsDataURL(file)
   228→    })
   229→  }, [])
   230→
   231→  const handleRemoveAttachment = useCallback((id: string) => {
   232→    setPendingAttachments((prev) => prev.filter((a) => a.id !== id))
   233→  }, [])
   234→
   235→  const handleDrop = useCallback(
   236→    (e: React.DragEvent) => {
   237→      e.preventDefault()
   238→      handleFileSelect(e.dataTransfer.files)
   239→    },
   240→    [handleFileSelect]
   241→  )
   242→
   243→  const handleDragOver = useCallback((e: React.DragEvent) => {
   244→    e.preventDefault()
   245→  }, [])
   246→
   247→  // Connection status indicator
   248→  const ConnectionIndicator = () => {
   249→    switch (connectionStatus) {
   250→      case 'connected':
   251→        return (
   252→
   253→            <Wifi size={12} />
   254→            Connected
   255→
   256→        )
   257→      case 'connecting':
   258→        return (
   259→
   260→            <Wifi size={12} className="animate-pulse" />
   261→            Connecting...
   262→
   263→        )
   264→      case 'error':
   265→        return (
   266→
   267→            <WifiOff size={12} />
   268→            Error
   269→
   270→        )
   271→      default:
   272→        return (
   273→
   274→            <WifiOff size={12} />
   275→            Disconnected
   276→
   277→        )
   278→    }
   279→  }
   280→
   281→  return (
   282→

   283→      {/* Header */}
   284→

   285→

   286→

   287→            {isImport ? 'Import Spec' : 'Create Spec'}: {projectName}
   288→

   289→          <ConnectionIndicator />
   290→

   291→
   292→

   293→          {isComplete && (
   294→
   295→              <CheckCircle2 size={16} />
   296→              Complete
   297→
   298→          )}
   299→
   300→          {/* Exit to Project - always visible escape hatch */}
   301→          <button
   302→            onClick={onExitToProject}
   303→            className="neo-btn neo-btn-ghost text-sm py-2"
   304→            title="Exit chat and go to project (you can start the agent manually)"
   305→          >
   306→            <ExternalLink size={16} />
   307→            Exit to Project
   308→          </button>
   309→
   310→          <button
   311→            onClick={onCancel}
   312→            className="neo-btn neo-btn-ghost p-2"
   313→            title="Cancel"
   314→          >
   315→            <X size={20} />
   316→          </button>
   317→

   318→

   319→
   320→      {/* Error banner */}
   321→      {error && (
   322→

   323→          <AlertCircle size={16} />
   324→          {error}
   325→          <button
   326→            onClick={() => setError(null)}
   327→            className="p-1 hover:bg-white/20 rounded"
   328→          >
   329→            <X size={14} />
   330→          </button>
   331→

   332→      )}
   333→
   334→      {/* Messages area */}
   335→

   336→        {messages.length === 0 && !isLoading && (
   337→

   338→

   339→

   340→                Starting Spec Creation
   341→

   342→

   343→                Connecting to Claude to help you create your app specification...
   344→

   345→              {connectionStatus === 'error' && (
   346→                <button
   347→                  onClick={start}
   348→                  className="neo-btn neo-btn-primary mt-4 text-sm"
   349→                >
   350→                  <RotateCcw size={14} />
   351→                  Retry Connection
   352→                </button>
   353→              )}
   354→

   355→

   356→        )}
   357→
   358→        {messages.map((message) => (
   359→          <ChatMessage key={message.id} message={message} />
   360→        ))}
   361→
   362→        {/* Structured questions */}
   363→        {currentQuestions && currentQuestions.length > 0 && (
   364→          <QuestionOptions
   365→            questions={currentQuestions}
   366→            onSubmit={handleAnswerSubmit}
   367→            disabled={isLoading}
   368→          />
   369→        )}
   370→
   371→        {/* Typing indicator - don't show when we have questions (waiting for user) */}
   372→        {isLoading && !currentQuestions && <TypingIndicator />}
   373→
   374→        {/* Scroll anchor */}
   375→

   376→

   377→
   378→      {/* Input area */}
   379→      {!isComplete && (
   380→

   385→          {/* Attachment previews */}
   386→          {pendingAttachments.length > 0 && (
   387→

   388→              {pendingAttachments.map((attachment) => (
   389→

   393→
   398→                  <button
   399→                    onClick={() => handleRemoveAttachment(attachment.id)}
   400→                    className="absolute -top-2 -right-2 bg-[var(--color-neo-danger)] text-white rounded-full p-0.5 border-2 border-[var(--color-neo-border)] hover:scale-110 transition-transform"
   401→                    title="Remove attachment"
   402→                  >
   403→                    <X size={12} />
   404→                  </button>
   405→
   406→                    {attachment.filename.length > 10
   407→                      ? `${attachment.filename.substring(0, 7)}...`
   408→                      : attachment.filename}
   409→
   410→

   411→              ))}
   412→

   413→          )}
   414→
   415→

   416→            {/* Hidden file input */}
   417→            <input
   418→              ref={fileInputRef}
   419→              type="file"
   420→              accept="image/jpeg,image/png"
   421→              multiple
   422→              onChange={(e) => handleFileSelect(e.target.files)}
   423→              className="hidden"
   424→            />
   425→
   426→            {/* Attach button */}
   427→            <button
   428→              onClick={() => fileInputRef.current?.click()}
   429→              disabled={connectionStatus !== 'connected'}
   430→              className="neo-btn neo-btn-ghost p-3"
   431→              title="Attach image (JPEG, PNG - max 5MB)"
   432→            >
   433→              <Paperclip size={18} />
   434→            </button>
   435→
   436→            <input
   437→              ref={inputRef}
   438→              type="text"
   439→              value={input}
   440→              onChange={(e) => setInput(e.target.value)}
   441→              onKeyDown={handleKeyDown}
   442→              placeholder={
   443→                currentQuestions
   444→                  ? 'Or type a custom response...'
   445→                  : pendingAttachments.length > 0
   446→                    ? 'Add a message with your image(s)...'
   447→                    : 'Type your response... (or /exit to go to project)'
   448→              }
   449→              className="neo-input flex-1"
   450→              disabled={(isLoading && !currentQuestions) || connectionStatus !== 'connected'}
   451→            />
   452→            <button
   453→              onClick={handleSendMessage}
   454→              disabled={
   455→                (!input.trim() && pendingAttachments.length === 0) ||
   456→                (isLoading && !currentQuestions) ||
   457→                connectionStatus !== 'connected'
   458→              }
   459→              className="neo-btn neo-btn-primary px-6"
   460→            >
   461→              <Send size={18} />
   462→            </button>
   463→

   464→
   465→          {/* Help text */}
   466→

   467→            Press Enter to send. Drag & drop or click <Paperclip size={12} className="inline" /> to attach images (JPEG/PNG, max 5MB).
   468→

   469→

   470→      )}
   471→
   472→      {/* Completion footer */}
   473→      {isComplete && (
   474→

   477→

   478→

   479→              {initializerStatus === 'starting' ? (
   480→                <>
   481→                  <Loader2 size={20} className="animate-spin" />
   482→
   483→                    Starting agent{yoloEnabled ? ' (YOLO mode)' : ''}...
   484→
   485→                </>
   486→              ) : initializerStatus === 'error' ? (
   487→                <>
   488→                  <AlertCircle size={20} className="text-white" />
   489→
   490→                    {initializerError || 'Failed to start agent'}
   491→
   492→                </>
   493→              ) : (
   494→                <>
   495→                  <CheckCircle2 size={20} />
   496→                  Specification created successfully!
   497→                </>
   498→              )}
   499→

   500→

   501→              {initializerStatus === 'error' && onRetryInitializer && (
   502→                <button
   503→                  onClick={onRetryInitializer}
   504→                  className="neo-btn bg-white"
   505→                >
   506→                  <RotateCcw size={14} />
   507→                  Retry
   508→                </button>
   509→              )}
   510→              {initializerStatus === 'idle' && (
   511→                <>
   512→                  {/* YOLO Mode Toggle */}
   513→                  <button
   514→                    onClick={() => setYoloEnabled(!yoloEnabled)}
   515→                    className={`neo-btn text-sm py-2 px-3 ${
   516→                      yoloEnabled ? 'neo-btn-warning' : 'bg-white'
   517→                    }`}
   518→                    title="YOLO Mode: Skip testing for rapid prototyping"
   519→                  >
   520→                    <Zap size={16} className={yoloEnabled ? 'text-yellow-900' : ''} />
   521→
   522→                      YOLO
   523→
   524→                  </button>
   525→                  <button
   526→                    onClick={() => onComplete('', yoloEnabled)}
   527→                    className="neo-btn neo-btn-primary"
   528→                  >
   529→                    Continue to Project
   530→                    <ArrowRight size={16} />
   531→                  </button>
   532→                </>
   533→              )}
   534→

   535→

   536→

   537→      )}
   538→

   539→  )
   540→}
   541→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * Spec Creation Chat Component
 *
 * Full chat interface for interactive spec creation with Claude.
 * Handles the 7-phase conversation flow for creating app specifications.
 */

import { useCallback, useEffect, useRef, useState } from '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'
import { TypingIndicator } from './TypingIndicator'
import type { ImageAttachment } from '../lib/types'
import type { CodebaseAnalysis } from '../lib/api'

// Image upload validation constants
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png']

type InitializerStatus = 'idle' | 'starting' | 'error'

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
  /** If true, use import-spec skill for existing codebases */
  isImport?: boolean
  /** If true, use refine-spec skill to modify existing spec */
  isRefine?: boolean
  /** Codebase analysis results from import */
  codebaseAnalysis?: CodebaseAnalysis | null
}

// Format codebase analysis for Claude context
function formatAnalysisForClaude(analysis: CodebaseAnalysis): string {
  const lines: string[] = [
    '# EXISTING PROJECT ANALYSIS',
    '',
    `**Project Directory:** ${analysis.project_name || 'Unknown'}`,
    '',
  ]

  if (Object.keys(analysis.detected_stack).length > 0) {
    lines.push('## Detected Technology Stack')
    for (const [category, tech] of Object.entries(analysis.detected_stack)) {
      lines.push(`- **${category}:** ${tech}`)
    }
    lines.push('')
  }

  if (analysis.directories.length > 0) {
    lines.push('## Directory Structure')
    lines.push('Top-level directories:')
    for (const dir of analysis.directories.slice(0, 10)) {
      lines.push(`- \`${dir}/\``)
    }
    if (analysis.directories.length > 10) {
      lines.push(`- ... and ${analysis.directories.length - 10} more`)
    }
    lines.push('')
  }

  if (Object.keys(analysis.file_types).length > 0) {
    lines.push('## File Statistics')
    lines.push(`Total code files: ${analysis.total_files}`)
    lines.push('')
    lines.push('Files by type:')
    for (const [ext, count] of Object.entries(analysis.file_types).slice(0, 8)) {
      lines.push(`- \`${ext}\`: ${count} files`)
    }
    lines.push('')
  }

  if (analysis.entry_points.length > 0) {
    lines.push('## Entry Points')
    for (const ep of analysis.entry_points.slice(0, 5)) {
      lines.push(`- ${ep}`)
    }
    lines.push('')
  }

  if (analysis.claude_md_content) {
    lines.push('## Project Documentation (from CLAUDE.md)')
    lines.push('')
    lines.push(analysis.claude_md_content)
    lines.push('')
  } else if (analysis.readme_content) {
    lines.push('## Project Documentation (from README.md)')
    lines.push('')
    lines.push(analysis.readme_content)
    lines.push('')
  }

  lines.push('---')
  lines.push('')

  return lines.join('\n')
}

export function SpecCreationChat({
  projectName,
  onComplete,
  onCancel,
  onExitToProject,
  initializerStatus = 'idle',
  initializerError = null,
  onRetryInitializer,
  isImport = false,
  isRefine = false,
  codebaseAnalysis = null,
}: SpecCreationChatProps) {
  const [input, setInput] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [yoloEnabled, setYoloEnabled] = useState(false)
  const [pendingAttachments, setPendingAttachments] = useState<ImageAttachment[]>([])
  const messagesEndRef = useRef<HTMLDivElement>(null)
  const inputRef = useRef<HTMLInputElement>(null)
  const fileInputRef = useRef<HTMLInputElement>(null)

  const {
    messages,
    isLoading,
    isComplete,
    connectionStatus,
    currentQuestions,
    start,
    sendMessage,
    sendAnswer,
    disconnect,
  } = useSpecChat({
    projectName,
    onComplete,
    onError: (err) => setError(err),
    isImport,
    codebaseAnalysis: codebaseAnalysis ? formatAnalysisForClaude(codebaseAnalysis) : null,
  })

  // Start the chat session when component mounts
  useEffect(() => {
    start()

    return () => {
      disconnect()
    }
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  // Scroll to bottom when messages change
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages, currentQuestions, isLoading])

  // Focus input when not loading and no questions
  useEffect(() => {
    if (!isLoading && !currentQuestions && inputRef.current) {
      inputRef.current.focus()
    }
  }, [isLoading, currentQuestions])

  const handleSendMessage = () => {
    const trimmed = input.trim()
    // 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
  }

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      handleSendMessage()
    }
  }

  const handleAnswerSubmit = (answers: Record<string, string | string[]>) => {
    sendAnswer(answers)
  }

  // File handling for image attachments
  const handleFileSelect = useCallback((files: FileList | null) => {
    if (!files) return

    Array.from(files).forEach((file) => {
      // Validate file type
      if (!ALLOWED_TYPES.includes(file.type)) {
        setError(`Invalid file type: ${file.name}. Only JPEG and PNG are supported.`)
        return
      }

      // Validate file size
      if (file.size > MAX_FILE_SIZE) {
        setError(`File too large: ${file.name}. Maximum size is 5 MB.`)
        return
      }

      // Read and convert to base64
      const reader = new FileReader()
      reader.onload = (e) => {
        const dataUrl = e.target?.result as string
        // dataUrl is "data:image/png;base64,XXXXXX"
        const base64Data = dataUrl.split(',')[1]

        const attachment: ImageAttachment = {
          id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
          filename: file.name,
          mimeType: file.type as 'image/jpeg' | 'image/png',
          base64Data,
          previewUrl: dataUrl,
          size: file.size,
        }

        setPendingAttachments((prev) => [...prev, attachment])
      }
      reader.readAsDataURL(file)
    })
  }, [])

  const handleRemoveAttachment = useCallback((id: string) => {
    setPendingAttachments((prev) => prev.filter((a) => a.id !== id))
  }, [])

  const handleDrop = useCallback(
    (e: React.DragEvent) => {
      e.preventDefault()
      handleFileSelect(e.dataTransfer.files)
    },
    [handleFileSelect]
  )

  const handleDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault()
  }, [])

  // Connection status indicator
  const ConnectionIndicator = () => {
    switch (connectionStatus) {
      case 'connected':
        return (

            <Wifi size={12} />
            Connected

        )
      case 'connecting':
        return (

            <Wifi size={12} className="animate-pulse" />
            Connecting...

        )
      case 'error':
        return (

            <WifiOff size={12} />
            Error

        )
      default:
        return (

            <WifiOff size={12} />
            Disconnected

        )
    }
  }

  return (

      {/* Header */}

            {isImport ? 'Import Spec' : 'Create Spec'}: {projectName}

          <ConnectionIndicator />

          {isComplete && (

              <CheckCircle2 size={16} />
              Complete

          )}

          {/* 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"
            title="Cancel"
          >
            <X size={20} />
          </button>

      {/* Error banner */}
      {error && (

          <AlertCircle size={16} />
          {error}
          <button
            onClick={() => setError(null)}
            className="p-1 hover:bg-white/20 rounded"
          >
            <X size={14} />
          </button>

      )}

      {/* Messages area */}

        {messages.length === 0 && !isLoading && (

                Starting Spec Creation

                Connecting to Claude to help you create your app specification...

              {connectionStatus === 'error' && (
                <button
                  onClick={start}
                  className="neo-btn neo-btn-primary mt-4 text-sm"
                >
                  <RotateCcw size={14} />
                  Retry Connection
                </button>
              )}

        )}

        {messages.map((message) => (
          <ChatMessage key={message.id} message={message} />
        ))}

        {/* Structured questions */}
        {currentQuestions && currentQuestions.length > 0 && (
          <QuestionOptions
            questions={currentQuestions}
            onSubmit={handleAnswerSubmit}
            disabled={isLoading}
          />
        )}

        {/* Typing indicator - don't show when we have questions (waiting for user) */}
        {isLoading && !currentQuestions && <TypingIndicator />}

        {/* Scroll anchor */}

      {/* Input area */}
      {!isComplete && (

          {/* Attachment previews */}
          {pendingAttachments.length > 0 && (

              {pendingAttachments.map((attachment) => (

                  <button
                    onClick={() => handleRemoveAttachment(attachment.id)}
                    className="absolute -top-2 -right-2 bg-[var(--color-neo-danger)] text-white rounded-full p-0.5 border-2 border-[var(--color-neo-border)] hover:scale-110 transition-transform"
                    title="Remove attachment"
                  >
                    <X size={12} />
                  </button>

                    {attachment.filename.length > 10
                      ? `${attachment.filename.substring(0, 7)}...`
                      : attachment.filename}

              ))}

          )}

            {/* Hidden file input */}
            <input
              ref={fileInputRef}
              type="file"
              accept="image/jpeg,image/png"
              multiple
              onChange={(e) => handleFileSelect(e.target.files)}
              className="hidden"
            />

            {/* Attach button */}
            <button
              onClick={() => fileInputRef.current?.click()}
              disabled={connectionStatus !== 'connected'}
              className="neo-btn neo-btn-ghost p-3"
              title="Attach image (JPEG, PNG - max 5MB)"
            >
              <Paperclip size={18} />
            </button>

            <input
              ref={inputRef}
              type="text"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={handleKeyDown}
              placeholder={
                currentQuestions
                  ? 'Or type a custom response...'
                  : pendingAttachments.length > 0
                    ? 'Add a message with your image(s)...'
                    : 'Type your response... (or /exit to go to project)'
              }
              className="neo-input flex-1"
              disabled={(isLoading && !currentQuestions) || connectionStatus !== 'connected'}
            />
            <button
              onClick={handleSendMessage}
              disabled={
                (!input.trim() && pendingAttachments.length === 0) ||
                (isLoading && !currentQuestions) ||
                connectionStatus !== 'connected'
              }
              className="neo-btn neo-btn-primary px-6"
            >
              <Send size={18} />
            </button>

          {/* Help text */}

            Press Enter to send. Drag & drop or click <Paperclip size={12} className="inline" /> to attach images (JPEG/PNG, max 5MB).

      )}

      {/* Completion footer */}
      {isComplete && (

              {initializerStatus === 'starting' ? (
                <>
                  <Loader2 size={20} className="animate-spin" />

                    Starting agent{yoloEnabled ? ' (YOLO mode)' : ''}...

                </>
              ) : initializerStatus === 'error' ? (
                <>
                  <AlertCircle size={20} className="text-white" />

                    {initializerError || 'Failed to start agent'}

                </>
              ) : (
                <>
                  <CheckCircle2 size={20} />
                  Specification created successfully!
                </>
              )}

              {initializerStatus === 'error' && onRetryInitializer && (
                <button
                  onClick={onRetryInitializer}
                  className="neo-btn bg-white"
                >
                  <RotateCcw size={14} />
                  Retry
                </button>
              )}
              {initializerStatus === 'idle' && (
                <>
                  {/* YOLO Mode Toggle */}
                  <button
                    onClick={() => setYoloEnabled(!yoloEnabled)}
                    className={`neo-btn text-sm py-2 px-3 ${
                      yoloEnabled ? 'neo-btn-warning' : 'bg-white'
                    }`}
                    title="YOLO Mode: Skip testing for rapid prototyping"
                  >
                    <Zap size={16} className={yoloEnabled ? 'text-yellow-900' : ''} />

                      YOLO

                  </button>
                  <button
                    onClick={() => onComplete('', yoloEnabled)}
                    className="neo-btn neo-btn-primary"
                  >
                    Continue to Project
                    <ArrowRight size={16} />
                  </button>
                </>
              )}

      )}

  )
}


1→import { useState, useEffect, useCallback } from 'react'
     2→import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
     3→import { useProjectWebSocket } from './hooks/useWebSocket'
     4→import { useFeatureSound } from './hooks/useFeatureSound'
     5→import { useCelebration } from './hooks/useCelebration'
     6→
     7→const STORAGE_KEY = 'autocoder-selected-project'
     8→import { ProjectSelector } from './components/ProjectSelector'
     9→import { KanbanBoard } from './components/KanbanBoard'
    10→import { AgentControl } from './components/AgentControl'
    11→import { ProgressDashboard } from './components/ProgressDashboard'
    12→import { SetupWizard } from './components/SetupWizard'
    13→import { AddFeatureForm } from './components/AddFeatureForm'
    14→import { FeatureModal } from './components/FeatureModal'
    15→import { DebugLogViewer } from './components/DebugLogViewer'
    16→import { AgentThought } from './components/AgentThought'
    17→import { AssistantFAB } from './components/AssistantFAB'
    18→import { AssistantPanel } from './components/AssistantPanel'
    19→import { GitPanel } from './components/GitPanel'
    20→import { TerminalChat } from './components/TerminalChat'
    21→import { ReferenceFoldersPanel } from './components/ReferenceFoldersPanel'
    22→import { Plus, Loader2, GitBranch, Terminal, X, FolderSearch } from 'lucide-react'
    23→import type { Feature } from './lib/types'
    24→
    25→function App() {
    26→  // Initialize selected project from localStorage
    27→  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    28→    try {
    29→      return localStorage.getItem(STORAGE_KEY)
    30→    } catch {
    31→      return null
    32→    }
    33→  })
    34→  const [showAddFeature, setShowAddFeature] = useState(false)
    35→  const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
    36→  const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
    37→  const [debugOpen, setDebugOpen] = useState(false)
    38→  const [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
    39→  const [assistantOpen, setAssistantOpen] = useState(false)
    40→  const [gitPanelOpen, setGitPanelOpen] = useState(false)
    41→  const [terminalOpen, setTerminalOpen] = useState(false)
    42→  const [referencesOpen, setReferencesOpen] = useState(false)
    43→
    44→  const { data: projects, isLoading: projectsLoading } = useProjects()
    45→  const { data: features } = useFeatures(selectedProject)
    46→  const { data: agentStatusData } = useAgentStatus(selectedProject)
    47→  const wsState = useProjectWebSocket(selectedProject)
    48→
    49→  // Play sounds when features move between columns
    50→  useFeatureSound(features)
    51→
    52→  // Celebrate when all features are complete
    53→  useCelebration(features, selectedProject)
    54→
    55→  // Persist selected project to localStorage
    56→  const handleSelectProject = useCallback((project: string | null) => {
    57→    setSelectedProject(project)
    58→    try {
    59→      if (project) {
    60→        localStorage.setItem(STORAGE_KEY, project)
    61→      } else {
    62→        localStorage.removeItem(STORAGE_KEY)
    63→      }
    64→    } catch {
    65→      // localStorage not available
    66→    }
    67→  }, [])
    68→
    69→  // Validate stored project exists (clear if project was deleted)
    70→  useEffect(() => {
    71→    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
    72→      handleSelectProject(null)
    73→    }
    74→  }, [selectedProject, projects, handleSelectProject])
    75→
    76→  // Keyboard shortcuts
    77→  useEffect(() => {
    78→    const handleKeyDown = (e: KeyboardEvent) => {
    79→      // Ignore if user is typing in an input
    80→      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
    81→        return
    82→      }
    83→
    84→      // D : Toggle debug window
    85→      if (e.key === 'd' || e.key === 'D') {
    86→        e.preventDefault()
    87→        setDebugOpen(prev => !prev)
    88→      }
    89→
    90→      // N : Add new feature (when project selected)
    91→      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
    92→        e.preventDefault()
    93→        setShowAddFeature(true)
    94→      }
    95→
    96→      // A : Toggle assistant panel (when project selected)
    97→      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
    98→        e.preventDefault()
    99→        setAssistantOpen(prev => !prev)
   100→      }
   101→
   102→      // G : Toggle git panel (when project selected)
   103→      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
   104→        e.preventDefault()
   105→        setGitPanelOpen(prev => !prev)
   106→      }
   107→
   108→      // T : Toggle terminal panel (when project selected)
   109→      if ((e.key === 't' || e.key === 'T') && selectedProject) {
   110→        e.preventDefault()
   111→        setTerminalOpen(prev => !prev)
   112→      }
   113→
   114→      // R : Toggle references panel (when project selected)
   115→      if ((e.key === 'r' || e.key === 'R') && selectedProject) {
   116→        e.preventDefault()
   117→        setReferencesOpen(prev => !prev)
   118→      }
   119→
   120→      // Escape : Close modals
   121→      if (e.key === 'Escape') {
   122→        if (referencesOpen) {
   123→          setReferencesOpen(false)
   124→        } else if (terminalOpen) {
   125→          setTerminalOpen(false)
   126→        } else if (gitPanelOpen) {
   127→          setGitPanelOpen(false)
   128→        } else if (assistantOpen) {
   129→          setAssistantOpen(false)
   130→        } else if (showAddFeature) {
   131→          setShowAddFeature(false)
   132→        } else if (selectedFeature) {
   133→          setSelectedFeature(null)
   134→        } else if (debugOpen) {
   135→          setDebugOpen(false)
   136→        }
   137→      }
   138→    }
   139→
   140→    window.addEventListener('keydown', handleKeyDown)
   141→    return () => window.removeEventListener('keydown', handleKeyDown)
   142→  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen, terminalOpen, referencesOpen])
   143→
   144→  // Combine WebSocket progress with feature data
   145→  const progress = wsState.progress.total > 0 ? wsState.progress : {
   146→    passing: features?.done.length ?? 0,
   147→    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
   148→    percentage: 0,
   149→  }
   150→
   151→  if (progress.total > 0 && progress.percentage === 0) {
   152→    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
   153→  }
   154→
   155→  if (!setupComplete) {
   156→    return <SetupWizard onComplete={() => setSetupComplete(true)} />
   157→  }
   158→
   159→  return (
   160→

   161→      {/* Header */}
   162→
   163→

   164→

   165→            {/* Logo and Title */}
   166→

   167→              AutoCoder
   168→

   169→
   170→            {/* Controls */}
   171→

   172→              <ProjectSelector
   173→                projects={projects ?? []}
   174→                selectedProject={selectedProject}
   175→                onSelectProject={handleSelectProject}
   176→                isLoading={projectsLoading}
   177→              />
   178→
   179→              {selectedProject && (
   180→                <>
   181→                  <button
   182→                    onClick={() => setShowAddFeature(true)}
   183→                    className="neo-btn neo-btn-primary text-sm"
   184→                    title="Press N"
   185→                  >
   186→                    <Plus size={18} />
   187→                    Add Feature
   188→
   189→                      N
   190→
   191→                  </button>
   192→
   193→                  <button
   194→                    onClick={() => setTerminalOpen(true)}
   195→                    className="neo-btn text-sm"
   196→                    title="Press T"
   197→                  >
   198→                    <Terminal size={18} />
   199→                    Terminal
   200→
   201→                      T
   202→
   203→                  </button>
   204→
   205→                  <button
   206→                    onClick={() => setReferencesOpen(true)}
   207→                    className="neo-btn text-sm"
   208→                    title="Press R"
   209→                  >
   210→                    <FolderSearch size={18} />
   211→                    Refs
   212→
   213→                      R
   214→
   215→                  </button>
   216→
   217→                  <button
   218→                    onClick={() => setGitPanelOpen(true)}
   219→                    className="neo-btn text-sm"
   220→                    title="Press G"
   221→                  >
   222→                    <GitBranch size={18} />
   223→                    Git
   224→
   225→                      G
   226→
   227→                  </button>
   228→
   229→                  <AgentControl
   230→                    projectName={selectedProject}
   231→                    status={wsState.agentStatus}
   232→                    yoloMode={agentStatusData?.yolo_mode ?? false}
   233→                  />
   234→                </>
   235→              )}
   236→

   237→

   238→

   239→
   240→
   241→      {/* Main Content */}
   242→
   246→        {!selectedProject ? (
   247→

   248→

   249→              Welcome to AutoCoder
   250→

   251→

   252→              Select a project from the dropdown above or create a new one to get started.
   253→

   254→

   255→        ) : (
   256→

   257→            {/* Progress Dashboard */}
   258→            <ProgressDashboard
   259→              passing={progress.passing}
   260→              total={progress.total}
   261→              percentage={progress.percentage}
   262→              isConnected={wsState.isConnected}
   263→            />
   264→
   265→            {/* Agent Thought - shows latest agent narrative */}
   266→            <AgentThought
   267→              logs={wsState.logs}
   268→              agentStatus={wsState.agentStatus}
   269→            />
   270→
   271→            {/* Initializing Features State - show when agent is running but no features yet */}
   272→            {features &&
   273→             features.pending.length === 0 &&
   274→             features.in_progress.length === 0 &&
   275→             features.done.length === 0 &&
   276→             wsState.agentStatus === 'running' && (
   277→

   278→                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />
   279→

   280→                  Initializing Features...
   281→

   282→

   283→                  The agent is reading your spec and creating features. This may take a moment.
   284→

   285→

   286→            )}
   287→
   288→            {/* Kanban Board */}
   289→            <KanbanBoard
   290→              features={features}
   291→              onFeatureClick={setSelectedFeature}
   292→            />
   293→

   294→        )}
   295→
   296→
   297→      {/* Add Feature Modal */}
   298→      {showAddFeature && selectedProject && (
   299→        <AddFeatureForm
   300→          projectName={selectedProject}
   301→          onClose={() => setShowAddFeature(false)}
   302→        />
   303→      )}
   304→
   305→      {/* Feature Detail Modal */}
   306→      {selectedFeature && selectedProject && (
   307→        <FeatureModal
   308→          feature={selectedFeature}
   309→          projectName={selectedProject}
   310→          onClose={() => setSelectedFeature(null)}
   311→        />
   312→      )}
   313→
   314→      {/* Debug Log Viewer - fixed to bottom */}
   315→      {selectedProject && (
   316→        <DebugLogViewer
   317→          logs={wsState.logs}
   318→          isOpen={debugOpen}
   319→          onToggle={() => setDebugOpen(!debugOpen)}
   320→          onClear={wsState.clearLogs}
   321→          onHeightChange={setDebugPanelHeight}
   322→        />
   323→      )}
   324→
   325→      {/* Assistant FAB and Panel */}
   326→      {selectedProject && (
   327→        <>
   328→          <AssistantFAB
   329→            onClick={() => setAssistantOpen(!assistantOpen)}
   330→            isOpen={assistantOpen}
   331→          />
   332→          <AssistantPanel
   333→            projectName={selectedProject}
   334→            isOpen={assistantOpen}
   335→            onClose={() => setAssistantOpen(false)}
   336→          />
   337→        </>
   338→      )}
   339→
   340→      {/* Git Panel */}
   341→      {selectedProject && (
   342→        <GitPanel
   343→          projectName={selectedProject}
   344→          isOpen={gitPanelOpen}
   345→          onClose={() => setGitPanelOpen(false)}
   346→        />
   347→      )}
   348→
   349→      {/* Terminal Panel */}
   350→      {selectedProject && (
   351→        <>
   352→          {/* Backdrop */}
   353→          {terminalOpen && (
   354→
 setTerminalOpen(false)}
   357→              aria-hidden="true"
   358→            />
   359→          )}
   360→          {/* Panel */}
   361→          <div
   362→            className={`
   363→              fixed right-0 top-0 bottom-0 z-50
   364→              w-[600px] max-w-[90vw]
   365→              bg-white
   366→              border-l-4 border-[var(--color-neo-border)]
   367→              shadow-[-8px_0_0px_rgba(0,0,0,1)]
   368→              transform transition-transform duration-300 ease-out
   369→              flex flex-col
   370→              ${terminalOpen ? 'translate-x-0' : 'translate-x-full'}
   371→            `}
   372→            role="dialog"
   373→            aria-label="Claude Terminal"
   374→            aria-hidden={!terminalOpen}
   375→          >
   376→            {/* Header */}
   377→

   378→

   379→

   380→                  <Terminal size={18} />
   381→

   382→

   383→
Claude Terminal

   384→
{selectedProject}

   385→

   386→

   387→              <button
   388→                onClick={() => setTerminalOpen(false)}
   389→                className="neo-btn neo-btn-ghost p-2 bg-white/20 border-white/40 hover:bg-white/30 text-white"
   390→                title="Close Terminal (Press T)"
   391→              >
   392→                <X size={18} />
   393→              </button>
   394→

   395→
   396→            {/* Terminal Chat area */}
   397→

   398→              {terminalOpen && <TerminalChat projectName={selectedProject} onClose={() => setTerminalOpen(false)} />}
   399→

   400→

   401→        </>
   402→      )}
   403→
   404→      {/* Reference Folders Panel */}
   405→      {selectedProject && (
   406→        <ReferenceFoldersPanel
   407→          projectName={selectedProject}
   408→          isOpen={referencesOpen}
   409→          onClose={() => setReferencesOpen(false)}
   410→        />
   411→      )}
   412→

   413→  )
   414→}
   415→
   416→export default App
   417→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import { useState, useEffect, useCallback } from 'react'
import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
import { useProjectWebSocket } from './hooks/useWebSocket'
import { useFeatureSound } from './hooks/useFeatureSound'
import { useCelebration } from './hooks/useCelebration'

const STORAGE_KEY = 'autocoder-selected-project'
import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal'
import { DebugLogViewer } from './components/DebugLogViewer'
import { AgentThought } from './components/AgentThought'
import { AssistantFAB } from './components/AssistantFAB'
import { AssistantPanel } from './components/AssistantPanel'
import { GitPanel } from './components/GitPanel'
import { TerminalChat } from './components/TerminalChat'
import { ReferenceFoldersPanel } from './components/ReferenceFoldersPanel'
import { Plus, Loader2, GitBranch, Terminal, X, FolderSearch } from 'lucide-react'
import type { Feature } from './lib/types'

function App() {
  // Initialize selected project from localStorage
  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    try {
      return localStorage.getItem(STORAGE_KEY)
    } catch {
      return null
    }
  })
  const [showAddFeature, setShowAddFeature] = useState(false)
  const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
  const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
  const [debugOpen, setDebugOpen] = useState(false)
  const [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
  const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)
  const [terminalOpen, setTerminalOpen] = useState(false)
  const [referencesOpen, setReferencesOpen] = useState(false)

  const { data: projects, isLoading: projectsLoading } = useProjects()
  const { data: features } = useFeatures(selectedProject)
  const { data: agentStatusData } = useAgentStatus(selectedProject)
  const wsState = useProjectWebSocket(selectedProject)

  // Play sounds when features move between columns
  useFeatureSound(features)

  // Celebrate when all features are complete
  useCelebration(features, selectedProject)

  // Persist selected project to localStorage
  const handleSelectProject = useCallback((project: string | null) => {
    setSelectedProject(project)
    try {
      if (project) {
        localStorage.setItem(STORAGE_KEY, project)
      } else {
        localStorage.removeItem(STORAGE_KEY)
      }
    } catch {
      // localStorage not available
    }
  }, [])

  // Validate stored project exists (clear if project was deleted)
  useEffect(() => {
    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
      handleSelectProject(null)
    }
  }, [selectedProject, projects, handleSelectProject])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Ignore if user is typing in an input
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
        return
      }

      // D : Toggle debug window
      if (e.key === 'd' || e.key === 'D') {
        e.preventDefault()
        setDebugOpen(prev => !prev)
      }

      // N : Add new feature (when project selected)
      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
        e.preventDefault()
        setShowAddFeature(true)
      }

      // A : Toggle assistant panel (when project selected)
      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
        e.preventDefault()
        setAssistantOpen(prev => !prev)
      }

      // G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // T : Toggle terminal panel (when project selected)
      if ((e.key === 't' || e.key === 'T') && selectedProject) {
        e.preventDefault()
        setTerminalOpen(prev => !prev)
      }

      // R : Toggle references panel (when project selected)
      if ((e.key === 'r' || e.key === 'R') && selectedProject) {
        e.preventDefault()
        setReferencesOpen(prev => !prev)
      }

      // Escape : Close modals
      if (e.key === 'Escape') {
        if (referencesOpen) {
          setReferencesOpen(false)
        } else if (terminalOpen) {
          setTerminalOpen(false)
        } else if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen, terminalOpen, referencesOpen])

  // Combine WebSocket progress with feature data
  const progress = wsState.progress.total > 0 ? wsState.progress : {
    passing: features?.done.length ?? 0,
    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
    percentage: 0,
  }

  if (progress.total > 0 && progress.percentage === 0) {
    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
  }

  if (!setupComplete) {
    return <SetupWizard onComplete={() => setSetupComplete(true)} />
  }

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

              <ProjectSelector
                projects={projects ?? []}
                selectedProject={selectedProject}
                onSelectProject={handleSelectProject}
                isLoading={projectsLoading}
              />

              {selectedProject && (
                <>
                  <button
                    onClick={() => setShowAddFeature(true)}
                    className="neo-btn neo-btn-primary text-sm"
                    title="Press N"
                  >
                    <Plus size={18} />
                    Add Feature

                      N

                  </button>

                  <button
                    onClick={() => setTerminalOpen(true)}
                    className="neo-btn text-sm"
                    title="Press T"
                  >
                    <Terminal size={18} />
                    Terminal

                      T

                  </button>

                  <button
                    onClick={() => setReferencesOpen(true)}
                    className="neo-btn text-sm"
                    title="Press R"
                  >
                    <FolderSearch size={18} />
                    Refs

                      R

                  </button>

                  <button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git

                      G

                  </button>

                  <AgentControl
                    projectName={selectedProject}
                    status={wsState.agentStatus}
                    yoloMode={agentStatusData?.yolo_mode ?? false}
                  />
                </>
              )}

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

              Select a project from the dropdown above or create a new one to get started.

        ) : (

            {/* Progress Dashboard */}
            <ProgressDashboard
              passing={progress.passing}
              total={progress.total}
              percentage={progress.percentage}
              isConnected={wsState.isConnected}
            />

            {/* Agent Thought - shows latest agent narrative */}
            <AgentThought
              logs={wsState.logs}
              agentStatus={wsState.agentStatus}
            />

            {/* Initializing Features State - show when agent is running but no features yet */}
            {features &&
             features.pending.length === 0 &&
             features.in_progress.length === 0 &&
             features.done.length === 0 &&
             wsState.agentStatus === 'running' && (

                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />

                  Initializing Features...

                  The agent is reading your spec and creating features. This may take a moment.

            )}

            {/* Kanban Board */}
            <KanbanBoard
              features={features}
              onFeatureClick={setSelectedFeature}
            />

        )}

      {/* Add Feature Modal */}
      {showAddFeature && selectedProject && (
        <AddFeatureForm
          projectName={selectedProject}
          onClose={() => setShowAddFeature(false)}
        />
      )}

      {/* Feature Detail Modal */}
      {selectedFeature && selectedProject && (
        <FeatureModal
          feature={selectedFeature}
          projectName={selectedProject}
          onClose={() => setSelectedFeature(null)}
        />
      )}

      {/* Debug Log Viewer - fixed to bottom */}
      {selectedProject && (
        <DebugLogViewer
          logs={wsState.logs}
          isOpen={debugOpen}
          onToggle={() => setDebugOpen(!debugOpen)}
          onClear={wsState.clearLogs}
          onHeightChange={setDebugPanelHeight}
        />
      )}

      {/* Assistant FAB and Panel */}
      {selectedProject && (
        <>
          <AssistantFAB
            onClick={() => setAssistantOpen(!assistantOpen)}
            isOpen={assistantOpen}
          />
          <AssistantPanel
            projectName={selectedProject}
            isOpen={assistantOpen}
            onClose={() => setAssistantOpen(false)}
          />
        </>
      )}

      {/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}

      {/* Terminal Panel */}
      {selectedProject && (
        <>
          {/* Backdrop */}
          {terminalOpen && (

 setTerminalOpen(false)}
              aria-hidden="true"
            />
          )}
          {/* Panel */}
          <div
            className={`
              fixed right-0 top-0 bottom-0 z-50
              w-[600px] max-w-[90vw]
              bg-white
              border-l-4 border-[var(--color-neo-border)]
              shadow-[-8px_0_0px_rgba(0,0,0,1)]
              transform transition-transform duration-300 ease-out
              flex flex-col
              ${terminalOpen ? 'translate-x-0' : 'translate-x-full'}
            `}
            role="dialog"
            aria-label="Claude Terminal"
            aria-hidden={!terminalOpen}
          >
            {/* Header */}

                  <Terminal size={18} />

Claude Terminal

{selectedProject}

              <button
                onClick={() => setTerminalOpen(false)}
                className="neo-btn neo-btn-ghost p-2 bg-white/20 border-white/40 hover:bg-white/30 text-white"
                title="Close Terminal (Press T)"
              >
                <X size={18} />
              </button>

            {/* Terminal Chat area */}

              {terminalOpen && <TerminalChat projectName={selectedProject} onClose={() => setTerminalOpen(false)} />}

        </>
      )}

      {/* Reference Folders Panel */}
      {selectedProject && (
        <ReferenceFoldersPanel
          projectName={selectedProject}
          isOpen={referencesOpen}
          onClose={() => setReferencesOpen(false)}
        />
      )}

  )
}

export default App


Looking at the code:

1. **SpecCreationChat.tsx** (lines 135-141) - I see the useSpecChat call but it's NOT passing `isRefine` yet:
```typescript
  const {
    messages,
    isLoading,
    ...
  } = useSpecChat({
    projectName,
    onComplete,
    onError: (err) => setError(err),
    isImport,
    codebaseAnalysis: codebaseAnalysis ? formatAnalysisForClaude(codebaseAnalysis) : null,
  })
```

I need to add `isRefine` to this call.

2. **App.tsx** - I see AgentControl is being used at line 229-233, but it's missing the `onRefineSpec` callback:
```typescript
<AgentControl
  projectName={selectedProject}
  status={wsState.agentStatus}
  yoloMode={agentStatusData?.yolo_mode ?? false}
/>
```

I need to:
1. Add `isRefine` to the useSpecChat call in SpecCreationChat.tsx
2. Add state for showing the spec refinement chat in App.tsx
3. Wire up the `onRefineSpec` callback in AgentControl

Let me make these changes.


I can see the issues. In SpecCreationChat.tsx, `isRefine` is accepted as a prop but not passed to `useSpecChat`. And in App.tsx, `AgentControl` doesn't have the `onRefineSpec` callback wired up. Let me fix both.


Edit

  } = useSpecChat({
    projectName,
    onComplete,
    onError: (err) => setError(err),
    isImport,
    codebaseAnalysis: codebaseAnalysis ? formatAnalysisForClaude(codebaseAnalysis) : null,
  })

  } = useSpecChat({
    projectName,
    onComplete,
    onError: (err) => setError(err),
    isImport,
    isRefine,
    codebaseAnalysis: codebaseAnalysis ? formatAnalysisForClaude(codebaseAnalysis) : null,
  })


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\ui\src\components\SpecCreationChat.tsx has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   131→    start,
   132→    sendMessage,
   133→    sendAnswer,
   134→    disconnect,
   135→  } = useSpecChat({
   136→    projectName,
   137→    onComplete,
   138→    onError: (err) => setError(err),
   139→    isImport,
   140→    isRefine,
   141→    codebaseAnalysis: codebaseAnalysis ? formatAnalysisForClaude(codebaseAnalysis) : null,
   142→  })
   143→
   144→  // Start the chat session when component mounts
   145→  useEffect(() => {
   146→    start()

  } = useSpecChat({
    projectName,
    onComplete,
    onError: (err) => setError(err),
    isImport,
    isRefine,
    codebaseAnalysis: codebaseAnalysis ? formatAnalysisForClaude(codebaseAnalysis) : null,
  })

/**
 * Spec Creation Chat Component
 *
 * Full chat interface for interactive spec creation with Claude.
 * Handles the 7-phase conversation flow for creating app specifications.
 */

import { useCallback, useEffect, useRef, useState } from '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'
import { TypingIndicator } from './TypingIndicator'
import type { ImageAttachment } from '../lib/types'
import type { CodebaseAnalysis } from '../lib/api'

// Image upload validation constants
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png']

type InitializerStatus = 'idle' | 'starting' | 'error'

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
  /** If true, use import-spec skill for existing codebases */
  isImport?: boolean
  /** If true, use refine-spec skill to modify existing spec */
  isRefine?: boolean
  /** Codebase analysis results from import */
  codebaseAnalysis?: CodebaseAnalysis | null
}

// Format codebase analysis for Claude context
function formatAnalysisForClaude(analysis: CodebaseAnalysis): string {
  const lines: string[] = [
    '# EXISTING PROJECT ANALYSIS',
    '',
    `**Project Directory:** ${analysis.project_name || 'Unknown'}`,
    '',
  ]

  if (Object.keys(analysis.detected_stack).length > 0) {
    lines.push('## Detected Technology Stack')
    for (const [category, tech] of Object.entries(analysis.detected_stack)) {
      lines.push(`- **${category}:** ${tech}`)
    }
    lines.push('')
  }

  if (analysis.directories.length > 0) {
    lines.push('## Directory Structure')
    lines.push('Top-level directories:')
    for (const dir of analysis.directories.slice(0, 10)) {
      lines.push(`- \`${dir}/\``)
    }
    if (analysis.directories.length > 10) {
      lines.push(`- ... and ${analysis.directories.length - 10} more`)
    }
    lines.push('')
  }

  if (Object.keys(analysis.file_types).length > 0) {
    lines.push('## File Statistics')
    lines.push(`Total code files: ${analysis.total_files}`)
    lines.push('')
    lines.push('Files by type:')
    for (const [ext, count] of Object.entries(analysis.file_types).slice(0, 8)) {
      lines.push(`- \`${ext}\`: ${count} files`)
    }
    lines.push('')
  }

  if (analysis.entry_points.length > 0) {
    lines.push('## Entry Points')
    for (const ep of analysis.entry_points.slice(0, 5)) {
      lines.push(`- ${ep}`)
    }
    lines.push('')
  }

  if (analysis.claude_md_content) {
    lines.push('## Project Documentation (from CLAUDE.md)')
    lines.push('')
    lines.push(analysis.claude_md_content)
    lines.push('')
  } else if (analysis.readme_content) {
    lines.push('## Project Documentation (from README.md)')
    lines.push('')
    lines.push(analysis.readme_content)
    lines.push('')
  }

  lines.push('---')
  lines.push('')

  return lines.join('\n')
}

export function SpecCreationChat({
  projectName,
  onComplete,
  onCancel,
  onExitToProject,
  initializerStatus = 'idle',
  initializerError = null,
  onRetryInitializer,
  isImport = false,
  isRefine = false,
  codebaseAnalysis = null,
}: SpecCreationChatProps) {
  const [input, setInput] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [yoloEnabled, setYoloEnabled] = useState(false)
  const [pendingAttachments, setPendingAttachments] = useState<ImageAttachment[]>([])
  const messagesEndRef = useRef<HTMLDivElement>(null)
  const inputRef = useRef<HTMLInputElement>(null)
  const fileInputRef = useRef<HTMLInputElement>(null)

  const {
    messages,
    isLoading,
    isComplete,
    connectionStatus,
    currentQuestions,
    start,
    sendMessage,
    sendAnswer,
    disconnect,
  } = useSpecChat({
    projectName,
    onComplete,
    onError: (err) => setError(err),
    isImport,
    codebaseAnalysis: codebaseAnalysis ? formatAnalysisForClaude(codebaseAnalysis) : null,
  })

  // Start the chat session when component mounts
  useEffect(() => {
    start()

    return () => {
      disconnect()
    }
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  // Scroll to bottom when messages change
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages, currentQuestions, isLoading])

  // Focus input when not loading and no questions
  useEffect(() => {
    if (!isLoading && !currentQuestions && inputRef.current) {
      inputRef.current.focus()
    }
  }, [isLoading, currentQuestions])

  const handleSendMessage = () => {
    const trimmed = input.trim()
    // 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
  }

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      handleSendMessage()
    }
  }

  const handleAnswerSubmit = (answers: Record<string, string | string[]>) => {
    sendAnswer(answers)
  }

  // File handling for image attachments
  const handleFileSelect = useCallback((files: FileList | null) => {
    if (!files) return

    Array.from(files).forEach((file) => {
      // Validate file type
      if (!ALLOWED_TYPES.includes(file.type)) {
        setError(`Invalid file type: ${file.name}. Only JPEG and PNG are supported.`)
        return
      }

      // Validate file size
      if (file.size > MAX_FILE_SIZE) {
        setError(`File too large: ${file.name}. Maximum size is 5 MB.`)
        return
      }

      // Read and convert to base64
      const reader = new FileReader()
      reader.onload = (e) => {
        const dataUrl = e.target?.result as string
        // dataUrl is "data:image/png;base64,XXXXXX"
        const base64Data = dataUrl.split(',')[1]

        const attachment: ImageAttachment = {
          id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
          filename: file.name,
          mimeType: file.type as 'image/jpeg' | 'image/png',
          base64Data,
          previewUrl: dataUrl,
          size: file.size,
        }

        setPendingAttachments((prev) => [...prev, attachment])
      }
      reader.readAsDataURL(file)
    })
  }, [])

  const handleRemoveAttachment = useCallback((id: string) => {
    setPendingAttachments((prev) => prev.filter((a) => a.id !== id))
  }, [])

  const handleDrop = useCallback(
    (e: React.DragEvent) => {
      e.preventDefault()
      handleFileSelect(e.dataTransfer.files)
    },
    [handleFileSelect]
  )

  const handleDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault()
  }, [])

  // Connection status indicator
  const ConnectionIndicator = () => {
    switch (connectionStatus) {
      case 'connected':
        return (

            <Wifi size={12} />
            Connected

        )
      case 'connecting':
        return (

            <Wifi size={12} className="animate-pulse" />
            Connecting...

        )
      case 'error':
        return (

            <WifiOff size={12} />
            Error

        )
      default:
        return (

            <WifiOff size={12} />
            Disconnected

        )
    }
  }

  return (

      {/* Header */}

            {isImport ? 'Import Spec' : 'Create Spec'}: {projectName}

          <ConnectionIndicator />

          {isComplete && (

              <CheckCircle2 size={16} />
              Complete

          )}

          {/* 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"
            title="Cancel"
          >
            <X size={20} />
          </button>

      {/* Error banner */}
      {error && (

          <AlertCircle size={16} />
          {error}
          <button
            onClick={() => setError(null)}
            className="p-1 hover:bg-white/20 rounded"
          >
            <X size={14} />
          </button>

      )}

      {/* Messages area */}

        {messages.length === 0 && !isLoading && (

                Starting Spec Creation

                Connecting to Claude to help you create your app specification...

              {connectionStatus === 'error' && (
                <button
                  onClick={start}
                  className="neo-btn neo-btn-primary mt-4 text-sm"
                >
                  <RotateCcw size={14} />
                  Retry Connection
                </button>
              )}

        )}

        {messages.map((message) => (
          <ChatMessage key={message.id} message={message} />
        ))}

        {/* Structured questions */}
        {currentQuestions && currentQuestions.length > 0 && (
          <QuestionOptions
            questions={currentQuestions}
            onSubmit={handleAnswerSubmit}
            disabled={isLoading}
          />
        )}

        {/* Typing indicator - don't show when we have questions (waiting for user) */}
        {isLoading && !currentQuestions && <TypingIndicator />}

        {/* Scroll anchor */}

      {/* Input area */}
      {!isComplete && (

          {/* Attachment previews */}
          {pendingAttachments.length > 0 && (

              {pendingAttachments.map((attachment) => (

                  <button
                    onClick={() => handleRemoveAttachment(attachment.id)}
                    className="absolute -top-2 -right-2 bg-[var(--color-neo-danger)] text-white rounded-full p-0.5 border-2 border-[var(--color-neo-border)] hover:scale-110 transition-transform"
                    title="Remove attachment"
                  >
                    <X size={12} />
                  </button>

                    {attachment.filename.length > 10
                      ? `${attachment.filename.substring(0, 7)}...`
                      : attachment.filename}

              ))}

          )}

            {/* Hidden file input */}
            <input
              ref={fileInputRef}
              type="file"
              accept="image/jpeg,image/png"
              multiple
              onChange={(e) => handleFileSelect(e.target.files)}
              className="hidden"
            />

            {/* Attach button */}
            <button
              onClick={() => fileInputRef.current?.click()}
              disabled={connectionStatus !== 'connected'}
              className="neo-btn neo-btn-ghost p-3"
              title="Attach image (JPEG, PNG - max 5MB)"
            >
              <Paperclip size={18} />
            </button>

            <input
              ref={inputRef}
              type="text"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={handleKeyDown}
              placeholder={
                currentQuestions
                  ? 'Or type a custom response...'
                  : pendingAttachments.length > 0
                    ? 'Add a message with your image(s)...'
                    : 'Type your response... (or /exit to go to project)'
              }
              className="neo-input flex-1"
              disabled={(isLoading && !currentQuestions) || connectionStatus !== 'connected'}
            />
            <button
              onClick={handleSendMessage}
              disabled={
                (!input.trim() && pendingAttachments.length === 0) ||
                (isLoading && !currentQuestions) ||
                connectionStatus !== 'connected'
              }
              className="neo-btn neo-btn-primary px-6"
            >
              <Send size={18} />
            </button>

          {/* Help text */}

            Press Enter to send. Drag & drop or click <Paperclip size={12} className="inline" /> to attach images (JPEG/PNG, max 5MB).

      )}

      {/* Completion footer */}
      {isComplete && (

              {initializerStatus === 'starting' ? (
                <>
                  <Loader2 size={20} className="animate-spin" />

                    Starting agent{yoloEnabled ? ' (YOLO mode)' : ''}...

                </>
              ) : initializerStatus === 'error' ? (
                <>
                  <AlertCircle size={20} className="text-white" />

                    {initializerError || 'Failed to start agent'}

                </>
              ) : (
                <>
                  <CheckCircle2 size={20} />
                  Specification created successfully!
                </>
              )}

              {initializerStatus === 'error' && onRetryInitializer && (
                <button
                  onClick={onRetryInitializer}
                  className="neo-btn bg-white"
                >
                  <RotateCcw size={14} />
                  Retry
                </button>
              )}
              {initializerStatus === 'idle' && (
                <>
                  {/* YOLO Mode Toggle */}
                  <button
                    onClick={() => setYoloEnabled(!yoloEnabled)}
                    className={`neo-btn text-sm py-2 px-3 ${
                      yoloEnabled ? 'neo-btn-warning' : 'bg-white'
                    }`}
                    title="YOLO Mode: Skip testing for rapid prototyping"
                  >
                    <Zap size={16} className={yoloEnabled ? 'text-yellow-900' : ''} />

                      YOLO

                  </button>
                  <button
                    onClick={() => onComplete('', yoloEnabled)}
                    className="neo-btn neo-btn-primary"
                  >
                    Continue to Project
                    <ArrowRight size={16} />
                  </button>
                </>
              )}

      )}

  )
}


Good. Now I need to update App.tsx to:
1. Add state for showing the spec refinement chat
2. Wire up the `onRefineSpec` callback in AgentControl
3. Add the SpecCreationChat component when refinement is active

Let me first add the import for SpecCreationChat if it's not there, then add the state and wire everything up.


Now I need to update App.tsx to add state for spec refinement and wire up the callback. Let me first add the import for SpecCreationChat.


import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'

import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { SpecCreationChat } from './components/SpecCreationChat'


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\ui\src\App.tsx has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
     4→import { useFeatureSound } from './hooks/useFeatureSound'
     5→import { useCelebration } from './hooks/useCelebration'
     6→
     7→const STORAGE_KEY = 'autocoder-selected-project'
     8→import { ProjectSelector } from './components/ProjectSelector'
     9→import { KanbanBoard } from './components/KanbanBoard'
    10→import { AgentControl } from './components/AgentControl'
    11→import { SpecCreationChat } from './components/SpecCreationChat'
    12→import { ProgressDashboard } from './components/ProgressDashboard'
    13→import { SetupWizard } from './components/SetupWizard'
    14→import { AddFeatureForm } from './components/AddFeatureForm'
    15→import { FeatureModal } from './components/FeatureModal'

import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { SpecCreationChat } from './components/SpecCreationChat'

import { useState, useEffect, useCallback } from 'react'
import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
import { useProjectWebSocket } from './hooks/useWebSocket'
import { useFeatureSound } from './hooks/useFeatureSound'
import { useCelebration } from './hooks/useCelebration'

const STORAGE_KEY = 'autocoder-selected-project'
import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal'
import { DebugLogViewer } from './components/DebugLogViewer'
import { AgentThought } from './components/AgentThought'
import { AssistantFAB } from './components/AssistantFAB'
import { AssistantPanel } from './components/AssistantPanel'
import { GitPanel } from './components/GitPanel'
import { TerminalChat } from './components/TerminalChat'
import { ReferenceFoldersPanel } from './components/ReferenceFoldersPanel'
import { Plus, Loader2, GitBranch, Terminal, X, FolderSearch } from 'lucide-react'
import type { Feature } from './lib/types'

function App() {
  // Initialize selected project from localStorage
  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    try {
      return localStorage.getItem(STORAGE_KEY)
    } catch {
      return null
    }
  })
  const [showAddFeature, setShowAddFeature] = useState(false)
  const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
  const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
  const [debugOpen, setDebugOpen] = useState(false)
  const [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
  const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)
  const [terminalOpen, setTerminalOpen] = useState(false)
  const [referencesOpen, setReferencesOpen] = useState(false)

  const { data: projects, isLoading: projectsLoading } = useProjects()
  const { data: features } = useFeatures(selectedProject)
  const { data: agentStatusData } = useAgentStatus(selectedProject)
  const wsState = useProjectWebSocket(selectedProject)

  // Play sounds when features move between columns
  useFeatureSound(features)

  // Celebrate when all features are complete
  useCelebration(features, selectedProject)

  // Persist selected project to localStorage
  const handleSelectProject = useCallback((project: string | null) => {
    setSelectedProject(project)
    try {
      if (project) {
        localStorage.setItem(STORAGE_KEY, project)
      } else {
        localStorage.removeItem(STORAGE_KEY)
      }
    } catch {
      // localStorage not available
    }
  }, [])

  // Validate stored project exists (clear if project was deleted)
  useEffect(() => {
    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
      handleSelectProject(null)
    }
  }, [selectedProject, projects, handleSelectProject])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Ignore if user is typing in an input
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
        return
      }

      // D : Toggle debug window
      if (e.key === 'd' || e.key === 'D') {
        e.preventDefault()
        setDebugOpen(prev => !prev)
      }

      // N : Add new feature (when project selected)
      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
        e.preventDefault()
        setShowAddFeature(true)
      }

      // A : Toggle assistant panel (when project selected)
      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
        e.preventDefault()
        setAssistantOpen(prev => !prev)
      }

      // G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // T : Toggle terminal panel (when project selected)
      if ((e.key === 't' || e.key === 'T') && selectedProject) {
        e.preventDefault()
        setTerminalOpen(prev => !prev)
      }

      // R : Toggle references panel (when project selected)
      if ((e.key === 'r' || e.key === 'R') && selectedProject) {
        e.preventDefault()
        setReferencesOpen(prev => !prev)
      }

      // Escape : Close modals
      if (e.key === 'Escape') {
        if (referencesOpen) {
          setReferencesOpen(false)
        } else if (terminalOpen) {
          setTerminalOpen(false)
        } else if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen, terminalOpen, referencesOpen])

  // Combine WebSocket progress with feature data
  const progress = wsState.progress.total > 0 ? wsState.progress : {
    passing: features?.done.length ?? 0,
    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
    percentage: 0,
  }

  if (progress.total > 0 && progress.percentage === 0) {
    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
  }

  if (!setupComplete) {
    return <SetupWizard onComplete={() => setSetupComplete(true)} />
  }

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

              <ProjectSelector
                projects={projects ?? []}
                selectedProject={selectedProject}
                onSelectProject={handleSelectProject}
                isLoading={projectsLoading}
              />

              {selectedProject && (
                <>
                  <button
                    onClick={() => setShowAddFeature(true)}
                    className="neo-btn neo-btn-primary text-sm"
                    title="Press N"
                  >
                    <Plus size={18} />
                    Add Feature

                      N

                  </button>

                  <button
                    onClick={() => setTerminalOpen(true)}
                    className="neo-btn text-sm"
                    title="Press T"
                  >
                    <Terminal size={18} />
                    Terminal

                      T

                  </button>

                  <button
                    onClick={() => setReferencesOpen(true)}
                    className="neo-btn text-sm"
                    title="Press R"
                  >
                    <FolderSearch size={18} />
                    Refs

                      R

                  </button>

                  <button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git

                      G

                  </button>

                  <AgentControl
                    projectName={selectedProject}
                    status={wsState.agentStatus}
                    yoloMode={agentStatusData?.yolo_mode ?? false}
                  />
                </>
              )}

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

              Select a project from the dropdown above or create a new one to get started.

        ) : (

            {/* Progress Dashboard */}
            <ProgressDashboard
              passing={progress.passing}
              total={progress.total}
              percentage={progress.percentage}
              isConnected={wsState.isConnected}
            />

            {/* Agent Thought - shows latest agent narrative */}
            <AgentThought
              logs={wsState.logs}
              agentStatus={wsState.agentStatus}
            />

            {/* Initializing Features State - show when agent is running but no features yet */}
            {features &&
             features.pending.length === 0 &&
             features.in_progress.length === 0 &&
             features.done.length === 0 &&
             wsState.agentStatus === 'running' && (

                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />

                  Initializing Features...

                  The agent is reading your spec and creating features. This may take a moment.

            )}

            {/* Kanban Board */}
            <KanbanBoard
              features={features}
              onFeatureClick={setSelectedFeature}
            />

        )}

      {/* Add Feature Modal */}
      {showAddFeature && selectedProject && (
        <AddFeatureForm
          projectName={selectedProject}
          onClose={() => setShowAddFeature(false)}
        />
      )}

      {/* Feature Detail Modal */}
      {selectedFeature && selectedProject && (
        <FeatureModal
          feature={selectedFeature}
          projectName={selectedProject}
          onClose={() => setSelectedFeature(null)}
        />
      )}

      {/* Debug Log Viewer - fixed to bottom */}
      {selectedProject && (
        <DebugLogViewer
          logs={wsState.logs}
          isOpen={debugOpen}
          onToggle={() => setDebugOpen(!debugOpen)}
          onClear={wsState.clearLogs}
          onHeightChange={setDebugPanelHeight}
        />
      )}

      {/* Assistant FAB and Panel */}
      {selectedProject && (
        <>
          <AssistantFAB
            onClick={() => setAssistantOpen(!assistantOpen)}
            isOpen={assistantOpen}
          />
          <AssistantPanel
            projectName={selectedProject}
            isOpen={assistantOpen}
            onClose={() => setAssistantOpen(false)}
          />
        </>
      )}

      {/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}

      {/* Terminal Panel */}
      {selectedProject && (
        <>
          {/* Backdrop */}
          {terminalOpen && (

 setTerminalOpen(false)}
              aria-hidden="true"
            />
          )}
          {/* Panel */}
          <div
            className={`
              fixed right-0 top-0 bottom-0 z-50
              w-[600px] max-w-[90vw]
              bg-white
              border-l-4 border-[var(--color-neo-border)]
              shadow-[-8px_0_0px_rgba(0,0,0,1)]
              transform transition-transform duration-300 ease-out
              flex flex-col
              ${terminalOpen ? 'translate-x-0' : 'translate-x-full'}
            `}
            role="dialog"
            aria-label="Claude Terminal"
            aria-hidden={!terminalOpen}
          >
            {/* Header */}

                  <Terminal size={18} />

Claude Terminal

{selectedProject}

              <button
                onClick={() => setTerminalOpen(false)}
                className="neo-btn neo-btn-ghost p-2 bg-white/20 border-white/40 hover:bg-white/30 text-white"
                title="Close Terminal (Press T)"
              >
                <X size={18} />
              </button>

            {/* Terminal Chat area */}

              {terminalOpen && <TerminalChat projectName={selectedProject} onClose={() => setTerminalOpen(false)} />}

        </>
      )}

      {/* Reference Folders Panel */}
      {selectedProject && (
        <ReferenceFoldersPanel
          projectName={selectedProject}
          isOpen={referencesOpen}
          onClose={() => setReferencesOpen(false)}
        />
      )}

  )
}

export default App

+import { SpecCreationChat } from './components/SpecCreationChat'


Now I need to add state for showing the spec refinement view. I'll add a state variable like `showSpecRefine`.


Now add state for spec refinement.


const [terminalOpen, setTerminalOpen] = useState(false)
  const [referencesOpen, setReferencesOpen] = useState(false)

const [terminalOpen, setTerminalOpen] = useState(false)
  const [referencesOpen, setReferencesOpen] = useState(false)
  const [showSpecRefine, setShowSpecRefine] = useState(false)