Explore the autocoder+ frontend to understand the project creation flow. Focus on:

1. NewProjectModal.tsx - how it manages steps, state, and creates projects
2. FolderBrowser.tsx - how folder selection works, what API it uses
3. useProjects.ts hook - how projects are created via API
4. The API client (api.ts) - what endpoints exist for projects

I need to understand:
- How the current "new project" wizard works step-by-step
- How FolderBrowser handles path selection and what callbacks it uses
- What data is sent when creating a project
- How we could add an "import" mode that selects an existing folder instead of creating a new one

Base path: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src


I'll explore the autocoder+ frontend to understand the project creation flow. Let me start by mapping out the key files you mentioned.


Perfect! I found all the files. Now let me read them to understand the project creation flow.


1→/**
     2→ * New Project Modal Component
     3→ *
     4→ * Multi-step modal for creating new projects:
     5→ * 1. Enter project name
     6→ * 2. Select project folder
     7→ * 3. Choose spec method (Claude or manual)
     8→ * 4a. If Claude: Show SpecCreationChat
     9→ * 4b. If manual: Create project and close
    10→ */
    11→
    12→import { useState } from 'react'
    13→import { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2, Folder } from 'lucide-react'
    14→import { useCreateProject } from '../hooks/useProjects'
    15→import { SpecCreationChat } from './SpecCreationChat'
    16→import { FolderBrowser } from './FolderBrowser'
    17→import { startAgent } from '../lib/api'
    18→
    19→type InitializerStatus = 'idle' | 'starting' | 'error'
    20→
    21→type Step = 'name' | 'folder' | 'method' | 'chat' | 'complete'
    22→type SpecMethod = 'claude' | 'manual'
    23→
    24→interface NewProjectModalProps {
    25→  isOpen: boolean
    26→  onClose: () => void
    27→  onProjectCreated: (projectName: string) => void
    28→}
    29→
    30→export function NewProjectModal({
    31→  isOpen,
    32→  onClose,
    33→  onProjectCreated,
    34→}: NewProjectModalProps) {
    35→  const [step, setStep] = useState<Step>('name')
    36→  const [projectName, setProjectName] = useState('')
    37→  const [projectPath, setProjectPath] = useState<string | null>(null)
    38→  const [_specMethod, setSpecMethod] = useState<SpecMethod | null>(null)
    39→  const [error, setError] = useState<string | null>(null)
    40→  const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
    41→  const [initializerError, setInitializerError] = useState<string | null>(null)
    42→  const [yoloModeSelected, setYoloModeSelected] = useState(false)
    43→
    44→  // Suppress unused variable warning - specMethod may be used in future
    45→  void _specMethod
    46→
    47→  const createProject = useCreateProject()
    48→
    49→  if (!isOpen) return null
    50→
    51→  const handleNameSubmit = (e: React.FormEvent) => {
    52→    e.preventDefault()
    53→    const trimmed = projectName.trim()
    54→
    55→    if (!trimmed) {
    56→      setError('Please enter a project name')
    57→      return
    58→    }
    59→
    60→    if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
    61→      setError('Project name can only contain letters, numbers, hyphens, and underscores')
    62→      return
    63→    }
    64→
    65→    setError(null)
    66→    setStep('folder')
    67→  }
    68→
    69→  const handleFolderSelect = (path: string) => {
    70→    // Append project name to the selected path
    71→    const fullPath = path.endsWith('/') ? `${path}${projectName.trim()}` : `${path}/${projectName.trim()}`
    72→    setProjectPath(fullPath)
    73→    setStep('method')
    74→  }
    75→
    76→  const handleFolderCancel = () => {
    77→    setStep('name')
    78→  }
    79→
    80→  const handleMethodSelect = async (method: SpecMethod) => {
    81→    setSpecMethod(method)
    82→
    83→    if (!projectPath) {
    84→      setError('Please select a project folder first')
    85→      setStep('folder')
    86→      return
    87→    }
    88→
    89→    if (method === 'manual') {
    90→      // Create project immediately with manual method
    91→      try {
    92→        const project = await createProject.mutateAsync({
    93→          name: projectName.trim(),
    94→          path: projectPath,
    95→          specMethod: 'manual',
    96→        })
    97→        setStep('complete')
    98→        setTimeout(() => {
    99→          onProjectCreated(project.name)
   100→          handleClose()
   101→        }, 1500)
   102→      } catch (err: unknown) {
   103→        setError(err instanceof Error ? err.message : 'Failed to create project')
   104→      }
   105→    } else {
   106→      // Create project then show chat
   107→      try {
   108→        await createProject.mutateAsync({
   109→          name: projectName.trim(),
   110→          path: projectPath,
   111→          specMethod: 'claude',
   112→        })
   113→        setStep('chat')
   114→      } catch (err: unknown) {
   115→        setError(err instanceof Error ? err.message : 'Failed to create project')
   116→      }
   117→    }
   118→  }
   119→
   120→  const handleSpecComplete = async (_specPath: string, yoloMode: boolean = false) => {
   121→    // Save yoloMode for retry
   122→    setYoloModeSelected(yoloMode)
   123→    // Auto-start the initializer agent
   124→    setInitializerStatus('starting')
   125→    try {
   126→      await startAgent(projectName.trim(), yoloMode)
   127→      // Success - navigate to project
   128→      setStep('complete')
   129→      setTimeout(() => {
   130→        onProjectCreated(projectName.trim())
   131→        handleClose()
   132→      }, 1500)
   133→    } catch (err) {
   134→      setInitializerStatus('error')
   135→      setInitializerError(err instanceof Error ? err.message : 'Failed to start agent')
   136→    }
   137→  }
   138→
   139→  const handleRetryInitializer = () => {
   140→    setInitializerError(null)
   141→    setInitializerStatus('idle')
   142→    handleSpecComplete('', yoloModeSelected)
   143→  }
   144→
   145→  const handleChatCancel = () => {
   146→    // Go back to method selection but keep the project
   147→    setStep('method')
   148→    setSpecMethod(null)
   149→  }
   150→
   151→  const handleExitToProject = () => {
   152→    // Exit chat and go directly to project - user can start agent manually
   153→    onProjectCreated(projectName.trim())
   154→    handleClose()
   155→  }
   156→
   157→  const handleClose = () => {
   158→    setStep('name')
   159→    setProjectName('')
   160→    setProjectPath(null)
   161→    setSpecMethod(null)
   162→    setError(null)
   163→    setInitializerStatus('idle')
   164→    setInitializerError(null)
   165→    setYoloModeSelected(false)
   166→    onClose()
   167→  }
   168→
   169→  const handleBack = () => {
   170→    if (step === 'method') {
   171→      setStep('folder')
   172→      setSpecMethod(null)
   173→    } else if (step === 'folder') {
   174→      setStep('name')
   175→      setProjectPath(null)
   176→    }
   177→  }
   178→
   179→  // Full-screen chat view
   180→  if (step === 'chat') {
   181→    return (
   182→

   183→        <SpecCreationChat
   184→          projectName={projectName.trim()}
   185→          onComplete={handleSpecComplete}
   186→          onCancel={handleChatCancel}
   187→          onExitToProject={handleExitToProject}
   188→          initializerStatus={initializerStatus}
   189→          initializerError={initializerError}
   190→          onRetryInitializer={handleRetryInitializer}
   191→        />
   192→

   193→    )
   194→  }
   195→
   196→  // Folder step uses larger modal
   197→  if (step === 'folder') {
   198→    return (
   199→

   200→
 e.stopPropagation()}
   203→        >
   204→          {/* Header */}
   205→

   206→

   207→              <Folder size={24} className="text-[var(--color-neo-progress)]" />
   208→

   209→

   210→                  Select Project Location
   211→

   212→

   213→                  A folder named {projectName} will be created inside the selected directory
   214→

   215→

   216→

   217→            <button
   218→              onClick={handleClose}
   219→              className="neo-btn neo-btn-ghost p-2"
   220→            >
   221→              <X size={20} />
   222→            </button>
   223→

   224→
   225→          {/* Folder Browser */}
   226→

   227→            <FolderBrowser
   228→              onSelect={handleFolderSelect}
   229→              onCancel={handleFolderCancel}
   230→            />
   231→

   232→

   233→

   234→    )
   235→  }
   236→
   237→  return (
   238→

   239→
 e.stopPropagation()}
   242→      >
   243→        {/* Header */}
   244→

   245→

   246→            {step === 'name' && 'Create New Project'}
   247→            {step === 'method' && 'Choose Setup Method'}
   248→            {step === 'complete' && 'Project Created!'}
   249→

   250→          <button
   251→            onClick={handleClose}
   252→            className="neo-btn neo-btn-ghost p-2"
   253→          >
   254→            <X size={20} />
   255→          </button>
   256→

   257→
   258→        {/* Content */}
   259→

   260→          {/* Step 1: Project Name */}
   261→          {step === 'name' && (
   262→            <form onSubmit={handleNameSubmit}>
   263→

   264→
   265→                  Project Name
   266→
   267→                <input
   268→                  type="text"
   269→                  value={projectName}
   270→                  onChange={(e) => setProjectName(e.target.value)}
   271→                  placeholder="my-awesome-app"
   272→                  className="neo-input"
   273→                  pattern="^[a-zA-Z0-9_-]+$"
   274→                  autoFocus
   275→                />
   276→

   277→                  Use letters, numbers, hyphens, and underscores only.
   278→

   279→

   280→
   281→              {error && (
   282→

   283→                  {error}
   284→

   285→              )}
   286→
   287→

   288→                <button
   289→                  type="submit"
   290→                  className="neo-btn neo-btn-primary"
   291→                  disabled={!projectName.trim()}
   292→                >
   293→                  Next
   294→                  <ArrowRight size={16} />
   295→                </button>
   296→

   297→            </form>
   298→          )}
   299→
   300→          {/* Step 2: Spec Method */}
   301→          {step === 'method' && (
   302→

   303→

   304→                How would you like to define your project?
   305→

   306→
   307→

   308→                {/* Claude option */}
   309→                <button
   310→                  onClick={() => handleMethodSelect('claude')}
   311→                  disabled={createProject.isPending}
   312→                  className={`
   313→                    w-full text-left p-4
   314→                    border-3 border-[var(--color-neo-border)]
   315→                    bg-white
   316→                    shadow-[4px_4px_0px_rgba(0,0,0,1)]
   317→                    hover:translate-x-[-2px] hover:translate-y-[-2px]
   318→                    hover:shadow-[6px_6px_0px_rgba(0,0,0,1)]
   319→                    transition-all duration-150
   320→                    disabled:opacity-50 disabled:cursor-not-allowed
   321→                  `}
   322→                >
   323→

   324→

   325→                      <Bot size={24} className="text-white" />
   326→

   327→

   328→

   329→                        Create with Claude
   330→
   331→                          Recommended
   332→
   333→

   334→

   335→                        Interactive conversation to define features and generate your app specification automatically.
   336→

   337→

   338→

   339→                </button>
   340→
   341→                {/* Manual option */}
   342→                <button
   343→                  onClick={() => handleMethodSelect('manual')}
   344→                  disabled={createProject.isPending}
   345→                  className={`
   346→                    w-full text-left p-4
   347→                    border-3 border-[var(--color-neo-border)]
   348→                    bg-white
   349→                    shadow-[4px_4px_0px_rgba(0,0,0,1)]
   350→                    hover:translate-x-[-2px] hover:translate-y-[-2px]
   351→                    hover:shadow-[6px_6px_0px_rgba(0,0,0,1)]
   352→                    transition-all duration-150
   353→                    disabled:opacity-50 disabled:cursor-not-allowed
   354→                  `}
   355→                >
   356→

   357→

   358→                      <FileEdit size={24} />
   359→

   360→

   361→                      Edit Templates Manually
   362→

   363→                        Edit the template files directly. Best for developers who want full control.
   364→

   365→

   366→

   367→                </button>
   368→

   369→
   370→              {error && (
   371→

   372→                  {error}
   373→

   374→              )}
   375→
   376→              {createProject.isPending && (
   377→

   378→                  <Loader2 size={16} className="animate-spin" />
   379→                  Creating project...
   380→

   381→              )}
   382→
   383→

   384→                <button
   385→                  onClick={handleBack}
   386→                  className="neo-btn neo-btn-ghost"
   387→                  disabled={createProject.isPending}
   388→                >
   389→                  <ArrowLeft size={16} />
   390→                  Back
   391→                </button>
   392→

   393→

   394→          )}
   395→
   396→          {/* Step 3: Complete */}
   397→          {step === 'complete' && (
   398→

   399→

   400→                <CheckCircle2 size={32} />
   401→

   402→

   403→                {projectName}
   404→

   405→

   406→                Your project has been created successfully!
   407→

   408→

   409→                <Loader2 size={16} className="animate-spin" />
   410→                Redirecting...
   411→

   412→

   413→          )}
   414→

   415→

   416→

   417→  )
   418→}
   419→

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

/**
 * New Project Modal Component
 *
 * Multi-step modal for creating new projects:
 * 1. Enter project name
 * 2. Select project folder
 * 3. Choose spec method (Claude or manual)
 * 4a. If Claude: Show SpecCreationChat
 * 4b. If manual: Create project and close
 */

import { useState } from 'react'
import { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2, Folder } from 'lucide-react'
import { useCreateProject } from '../hooks/useProjects'
import { SpecCreationChat } from './SpecCreationChat'
import { FolderBrowser } from './FolderBrowser'
import { startAgent } from '../lib/api'

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

type Step = 'name' | 'folder' | 'method' | 'chat' | 'complete'
type SpecMethod = 'claude' | 'manual'

interface NewProjectModalProps {
  isOpen: boolean
  onClose: () => void
  onProjectCreated: (projectName: string) => void
}

export function NewProjectModal({
  isOpen,
  onClose,
  onProjectCreated,
}: NewProjectModalProps) {
  const [step, setStep] = useState<Step>('name')
  const [projectName, setProjectName] = useState('')
  const [projectPath, setProjectPath] = useState<string | null>(null)
  const [_specMethod, setSpecMethod] = useState<SpecMethod | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
  const [initializerError, setInitializerError] = useState<string | null>(null)
  const [yoloModeSelected, setYoloModeSelected] = useState(false)

  // Suppress unused variable warning - specMethod may be used in future
  void _specMethod

  const createProject = useCreateProject()

  if (!isOpen) return null

  const handleNameSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    const trimmed = projectName.trim()

    if (!trimmed) {
      setError('Please enter a project name')
      return
    }

    if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
      setError('Project name can only contain letters, numbers, hyphens, and underscores')
      return
    }

    setError(null)
    setStep('folder')
  }

  const handleFolderSelect = (path: string) => {
    // Append project name to the selected path
    const fullPath = path.endsWith('/') ? `${path}${projectName.trim()}` : `${path}/${projectName.trim()}`
    setProjectPath(fullPath)
    setStep('method')
  }

  const handleFolderCancel = () => {
    setStep('name')
  }

  const handleMethodSelect = async (method: SpecMethod) => {
    setSpecMethod(method)

    if (!projectPath) {
      setError('Please select a project folder first')
      setStep('folder')
      return
    }

    if (method === 'manual') {
      // Create project immediately with manual method
      try {
        const project = await createProject.mutateAsync({
          name: projectName.trim(),
          path: projectPath,
          specMethod: 'manual',
        })
        setStep('complete')
        setTimeout(() => {
          onProjectCreated(project.name)
          handleClose()
        }, 1500)
      } catch (err: unknown) {
        setError(err instanceof Error ? err.message : 'Failed to create project')
      }
    } else {
      // Create project then show chat
      try {
        await createProject.mutateAsync({
          name: projectName.trim(),
          path: projectPath,
          specMethod: 'claude',
        })
        setStep('chat')
      } catch (err: unknown) {
        setError(err instanceof Error ? err.message : 'Failed to create project')
      }
    }
  }

  const handleSpecComplete = async (_specPath: string, yoloMode: boolean = false) => {
    // Save yoloMode for retry
    setYoloModeSelected(yoloMode)
    // Auto-start the initializer agent
    setInitializerStatus('starting')
    try {
      await startAgent(projectName.trim(), yoloMode)
      // Success - navigate to project
      setStep('complete')
      setTimeout(() => {
        onProjectCreated(projectName.trim())
        handleClose()
      }, 1500)
    } catch (err) {
      setInitializerStatus('error')
      setInitializerError(err instanceof Error ? err.message : 'Failed to start agent')
    }
  }

  const handleRetryInitializer = () => {
    setInitializerError(null)
    setInitializerStatus('idle')
    handleSpecComplete('', yoloModeSelected)
  }

  const handleChatCancel = () => {
    // Go back to method selection but keep the project
    setStep('method')
    setSpecMethod(null)
  }

  const handleExitToProject = () => {
    // Exit chat and go directly to project - user can start agent manually
    onProjectCreated(projectName.trim())
    handleClose()
  }

  const handleClose = () => {
    setStep('name')
    setProjectName('')
    setProjectPath(null)
    setSpecMethod(null)
    setError(null)
    setInitializerStatus('idle')
    setInitializerError(null)
    setYoloModeSelected(false)
    onClose()
  }

  const handleBack = () => {
    if (step === 'method') {
      setStep('folder')
      setSpecMethod(null)
    } else if (step === 'folder') {
      setStep('name')
      setProjectPath(null)
    }
  }

  // Full-screen chat view
  if (step === 'chat') {
    return (

        <SpecCreationChat
          projectName={projectName.trim()}
          onComplete={handleSpecComplete}
          onCancel={handleChatCancel}
          onExitToProject={handleExitToProject}
          initializerStatus={initializerStatus}
          initializerError={initializerError}
          onRetryInitializer={handleRetryInitializer}
        />

    )
  }

  // Folder step uses larger modal
  if (step === 'folder') {
    return (

 e.stopPropagation()}
        >
          {/* Header */}

              <Folder size={24} className="text-[var(--color-neo-progress)]" />

                  Select Project Location

                  A folder named {projectName} will be created inside the selected directory

            <button
              onClick={handleClose}
              className="neo-btn neo-btn-ghost p-2"
            >
              <X size={20} />
            </button>

          {/* Folder Browser */}

            <FolderBrowser
              onSelect={handleFolderSelect}
              onCancel={handleFolderCancel}
            />

    )
  }

  return (

 e.stopPropagation()}
      >
        {/* Header */}

            {step === 'name' && 'Create New Project'}
            {step === 'method' && 'Choose Setup Method'}
            {step === 'complete' && 'Project Created!'}

          <button
            onClick={handleClose}
            className="neo-btn neo-btn-ghost p-2"
          >
            <X size={20} />
          </button>

        {/* Content */}

          {/* Step 1: Project Name */}
          {step === 'name' && (
            <form onSubmit={handleNameSubmit}>

                  Project Name

                <input
                  type="text"
                  value={projectName}
                  onChange={(e) => setProjectName(e.target.value)}
                  placeholder="my-awesome-app"
                  className="neo-input"
                  pattern="^[a-zA-Z0-9_-]+$"
                  autoFocus
                />

                  Use letters, numbers, hyphens, and underscores only.

              {error && (

                  {error}

              )}

                <button
                  type="submit"
                  className="neo-btn neo-btn-primary"
                  disabled={!projectName.trim()}
                >
                  Next
                  <ArrowRight size={16} />
                </button>

            </form>
          )}

          {/* Step 2: Spec Method */}
          {step === 'method' && (

                How would you like to define your project?

                {/* Claude option */}
                <button
                  onClick={() => handleMethodSelect('claude')}
                  disabled={createProject.isPending}
                  className={`
                    w-full text-left p-4
                    border-3 border-[var(--color-neo-border)]
                    bg-white
                    shadow-[4px_4px_0px_rgba(0,0,0,1)]
                    hover:translate-x-[-2px] hover:translate-y-[-2px]
                    hover:shadow-[6px_6px_0px_rgba(0,0,0,1)]
                    transition-all duration-150
                    disabled:opacity-50 disabled:cursor-not-allowed
                  `}
                >

                      <Bot size={24} className="text-white" />

                        Create with Claude

                          Recommended

                        Interactive conversation to define features and generate your app specification automatically.

                </button>

                {/* Manual option */}
                <button
                  onClick={() => handleMethodSelect('manual')}
                  disabled={createProject.isPending}
                  className={`
                    w-full text-left p-4
                    border-3 border-[var(--color-neo-border)]
                    bg-white
                    shadow-[4px_4px_0px_rgba(0,0,0,1)]
                    hover:translate-x-[-2px] hover:translate-y-[-2px]
                    hover:shadow-[6px_6px_0px_rgba(0,0,0,1)]
                    transition-all duration-150
                    disabled:opacity-50 disabled:cursor-not-allowed
                  `}
                >

                      <FileEdit size={24} />

                      Edit Templates Manually

                        Edit the template files directly. Best for developers who want full control.

                </button>

              {error && (

                  {error}

              )}

              {createProject.isPending && (

                  <Loader2 size={16} className="animate-spin" />
                  Creating project...

              )}

                <button
                  onClick={handleBack}
                  className="neo-btn neo-btn-ghost"
                  disabled={createProject.isPending}
                >
                  <ArrowLeft size={16} />
                  Back
                </button>

          )}

          {/* Step 3: Complete */}
          {step === 'complete' && (

                <CheckCircle2 size={32} />

                {projectName}

                Your project has been created successfully!

                <Loader2 size={16} className="animate-spin" />
                Redirecting...

          )}

  )
}


1→/**
     2→ * Folder Browser Component
     3→ *
     4→ * Server-side filesystem browser for selecting project directories.
     5→ * Cross-platform support for Windows, macOS, and Linux.
     6→ */
     7→
     8→import { useState, useEffect } from 'react'
     9→import { useQuery } from '@tanstack/react-query'
    10→import {
    11→  Folder,
    12→  FolderOpen,
    13→  ChevronRight,
    14→  HardDrive,
    15→  Loader2,
    16→  AlertCircle,
    17→  FolderPlus,
    18→  ArrowLeft,
    19→} from 'lucide-react'
    20→import * as api from '../lib/api'
    21→import type { DirectoryEntry, DriveInfo } from '../lib/types'
    22→
    23→interface FolderBrowserProps {
    24→  onSelect: (path: string) => void
    25→  onCancel: () => void
    26→  initialPath?: string
    27→}
    28→
    29→export function FolderBrowser({ onSelect, onCancel, initialPath }: FolderBrowserProps) {
    30→  const [currentPath, setCurrentPath] = useState<string | undefined>(initialPath)
    31→  const [selectedPath, setSelectedPath] = useState<string | null>(null)
    32→  const [isCreatingFolder, setIsCreatingFolder] = useState(false)
    33→  const [newFolderName, setNewFolderName] = useState('')
    34→  const [createError, setCreateError] = useState<string | null>(null)
    35→
    36→  // Fetch directory listing
    37→  const {
    38→    data: directoryData,
    39→    isLoading,
    40→    error,
    41→    refetch,
    42→  } = useQuery({
    43→    queryKey: ['filesystem', 'list', currentPath],
    44→    queryFn: () => api.listDirectory(currentPath),
    45→  })
    46→
    47→  // Update selected path when directory changes
    48→  useEffect(() => {
    49→    if (directoryData?.current_path) {
    50→      setSelectedPath(directoryData.current_path)
    51→    }
    52→  }, [directoryData?.current_path])
    53→
    54→  const handleNavigate = (path: string) => {
    55→    setCurrentPath(path)
    56→    setSelectedPath(path)
    57→    setIsCreatingFolder(false)
    58→    setNewFolderName('')
    59→    setCreateError(null)
    60→  }
    61→
    62→  const handleNavigateUp = () => {
    63→    if (directoryData?.parent_path) {
    64→      handleNavigate(directoryData.parent_path)
    65→    }
    66→  }
    67→
    68→  const handleDriveSelect = (drive: DriveInfo) => {
    69→    handleNavigate(`${drive.letter}:/`)
    70→  }
    71→
    72→  const handleEntryClick = (entry: DirectoryEntry) => {
    73→    if (entry.is_directory) {
    74→      handleNavigate(entry.path)
    75→    }
    76→  }
    77→
    78→  const handleCreateFolder = async () => {
    79→    if (!newFolderName.trim()) {
    80→      setCreateError('Folder name is required')
    81→      return
    82→    }
    83→
    84→    // Basic validation
    85→    if (!/^[a-zA-Z0-9_\-. ]+$/.test(newFolderName)) {
    86→      setCreateError('Invalid folder name')
    87→      return
    88→    }
    89→
    90→    const newPath = `${directoryData?.current_path}/${newFolderName.trim()}`
    91→
    92→    try {
    93→      await api.createDirectory(newPath)
    94→      // Refresh the directory listing
    95→      await refetch()
    96→      // Navigate to the new folder
    97→      handleNavigate(newPath)
    98→    } catch (err) {
    99→      setCreateError(err instanceof Error ? err.message : 'Failed to create folder')
   100→    }
   101→  }
   102→
   103→  const handleSelect = () => {
   104→    if (selectedPath) {
   105→      onSelect(selectedPath)
   106→    }
   107→  }
   108→
   109→  // Parse breadcrumb segments from path
   110→  const getBreadcrumbs = (path: string): { name: string; path: string }[] => {
   111→    if (!path) return []
   112→
   113→    const segments: { name: string; path: string }[] = []
   114→
   115→    // Handle Windows drive letters
   116→    if (/^[A-Za-z]:/.test(path)) {
   117→      const drive = path.slice(0, 2)
   118→      segments.push({ name: drive, path: `${drive}/` })
   119→      path = path.slice(3)
   120→    } else if (path.startsWith('/')) {
   121→      segments.push({ name: '/', path: '/' })
   122→      path = path.slice(1)
   123→    }
   124→
   125→    // Split remaining path
   126→    const parts = path.split('/').filter(Boolean)
   127→    let currentPath = segments.length > 0 ? segments[0].path : ''
   128→
   129→    for (const part of parts) {
   130→      currentPath = currentPath.endsWith('/') ? currentPath + part : currentPath + '/' + part
   131→      segments.push({ name: part, path: currentPath })
   132→    }
   133→
   134→    return segments
   135→  }
   136→
   137→  const breadcrumbs = directoryData?.current_path ? getBreadcrumbs(directoryData.current_path) : []
   138→
   139→  return (
   140→

   141→      {/* Header with breadcrumb navigation */}
   142→

   143→

   144→          <Folder size={20} className="text-[var(--color-neo-progress)]" />
   145→          Select Project Folder
   146→

   147→
   148→        {/* Breadcrumb navigation */}
   149→

   150→          {directoryData?.parent_path && (
   151→            <button
   152→              onClick={handleNavigateUp}
   153→              className="neo-btn neo-btn-ghost p-1"
   154→              title="Go up"
   155→            >
   156→              <ArrowLeft size={16} />
   157→            </button>
   158→          )}
   159→
   160→          {breadcrumbs.map((crumb, index) => (
   161→

   162→              {index > 0 && <ChevronRight size={14} className="text-gray-400 mx-1" />}
   163→              <button
   164→                onClick={() => handleNavigate(crumb.path)}
   165→                className={`
   166→                  px-2 py-1 rounded text-[#1a1a1a]
   167→                  hover:bg-[var(--color-neo-bg)]
   168→                  ${index === breadcrumbs.length - 1 ? 'font-bold' : ''}
   169→                `}
   170→              >
   171→                {crumb.name}
   172→              </button>
   173→

   174→          ))}
   175→

   176→

   177→
   178→      {/* Drive selector (Windows only) */}
   179→      {directoryData?.drives && directoryData.drives.length > 0 && (
   180→

   181→

   182→            Drives:
   183→            {directoryData.drives.map((drive) => (
   184→              <button
   185→                key={drive.letter}
   186→                onClick={() => handleDriveSelect(drive)}
   187→                className={`
   188→                  neo-btn neo-btn-ghost py-1 px-2 text-sm
   189→                  flex items-center gap-1
   190→                  ${currentPath?.startsWith(drive.letter) ? 'bg-[var(--color-neo-progress)] text-white' : ''}
   191→                `}
   192→              >
   193→                <HardDrive size={14} />
   194→                {drive.letter}: {drive.label && `(${drive.label})`}
   195→              </button>
   196→            ))}
   197→

   198→

   199→      )}
   200→
   201→      {/* Directory listing */}
   202→

   203→        {isLoading ? (
   204→

   205→            <Loader2 size={24} className="animate-spin text-[var(--color-neo-progress)]" />
   206→

   207→        ) : error ? (
   208→

   209→            <AlertCircle size={32} className="mx-auto mb-2 text-[var(--color-neo-danger)]" />
   210→

   211→              {error instanceof Error ? error.message : 'Failed to load directory'}
   212→

   213→            <button onClick={() => refetch()} className="neo-btn neo-btn-ghost mt-2">
   214→              Retry
   215→            </button>
   216→

   217→        ) : (
   218→

   219→            {/* Directory entries - only show directories */}
   220→            {directoryData?.entries
   221→              .filter((entry) => entry.is_directory)
   222→              .map((entry) => (
   223→                <button
   224→                  key={entry.path}
   225→                  onClick={() => handleEntryClick(entry)}
   226→                  onDoubleClick={() => handleNavigate(entry.path)}
   227→                  className={`
   228→                    w-full text-left p-2 rounded
   229→                    flex items-center gap-2
   230→                    hover:bg-[var(--color-neo-bg)]
   231→                    border-2 border-transparent
   232→                    text-[var(--color-neo-text)]
   233→                    ${selectedPath === entry.path ? 'bg-[var(--color-neo-progress)] bg-opacity-10 border-[var(--color-neo-progress)]' : ''}
   234→                  `}
   235→                >
   236→                  {selectedPath === entry.path ? (
   237→                    <FolderOpen size={18} className="text-[var(--color-neo-progress)] flex-shrink-0" />
   238→                  ) : (
   239→                    <Folder size={18} className="text-[var(--color-neo-pending)] flex-shrink-0" />
   240→                  )}
   241→                  {entry.name}
   242→                  {entry.has_children && (
   243→                    <ChevronRight size={14} className="ml-auto text-gray-400 flex-shrink-0" />
   244→                  )}
   245→                </button>
   246→              ))}
   247→
   248→            {/* Empty state */}
   249→            {directoryData?.entries.filter((e) => e.is_directory).length === 0 && (
   250→

   251→                <Folder size={32} className="mx-auto mb-2 opacity-50" />
   252→
No subfolders

   253→
You can create a new folder or select this directory.

   254→

   255→            )}
   256→

   257→        )}
   258→
   259→        {/* New folder creation */}
   260→        {isCreatingFolder && (
   261→

   262→

   263→              <FolderPlus size={18} className="text-[var(--color-neo-progress)]" />
   264→              <input
   265→                type="text"
   266→                value={newFolderName}
   267→                onChange={(e) => setNewFolderName(e.target.value)}
   268→                placeholder="New folder name"
   269→                className="neo-input flex-1 py-1"
   270→                autoFocus
   271→                onKeyDown={(e) => {
   272→                  if (e.key === 'Enter') handleCreateFolder()
   273→                  if (e.key === 'Escape') {
   274→                    setIsCreatingFolder(false)
   275→                    setNewFolderName('')
   276→                    setCreateError(null)
   277→                  }
   278→                }}
   279→              />
   280→              <button onClick={handleCreateFolder} className="neo-btn neo-btn-primary py-1 px-3">
   281→                Create
   282→              </button>
   283→              <button
   284→                onClick={() => {
   285→                  setIsCreatingFolder(false)
   286→                  setNewFolderName('')
   287→                  setCreateError(null)
   288→                }}
   289→                className="neo-btn neo-btn-ghost py-1 px-2"
   290→              >
   291→                Cancel
   292→              </button>
   293→

   294→            {createError && (
   295→
{createError}

   296→            )}
   297→

   298→        )}
   299→

   300→
   301→      {/* Footer with selected path and actions */}
   302→

   303→        {/* Selected path display */}
   304→

   305→
Selected path:

   306→
{selectedPath || 'No folder selected'}

   307→

   308→
   309→        {/* Actions */}
   310→

   311→          <button
   312→            onClick={() => setIsCreatingFolder(true)}
   313→            className="neo-btn neo-btn-ghost"
   314→            disabled={isCreatingFolder}
   315→          >
   316→            <FolderPlus size={16} />
   317→            New Folder
   318→          </button>
   319→
   320→

   321→            <button onClick={onCancel} className="neo-btn neo-btn-ghost">
   322→              Cancel
   323→            </button>
   324→            <button
   325→              onClick={handleSelect}
   326→              className="neo-btn neo-btn-primary"
   327→              disabled={!selectedPath}
   328→            >
   329→              Select This Folder
   330→            </button>
   331→

   332→

   333→

   334→

   335→  )
   336→}
   337→

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

/**
 * Folder Browser Component
 *
 * Server-side filesystem browser for selecting project directories.
 * Cross-platform support for Windows, macOS, and Linux.
 */

import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import {
  Folder,
  FolderOpen,
  ChevronRight,
  HardDrive,
  Loader2,
  AlertCircle,
  FolderPlus,
  ArrowLeft,
} from 'lucide-react'
import * as api from '../lib/api'
import type { DirectoryEntry, DriveInfo } from '../lib/types'

interface FolderBrowserProps {
  onSelect: (path: string) => void
  onCancel: () => void
  initialPath?: string
}

export function FolderBrowser({ onSelect, onCancel, initialPath }: FolderBrowserProps) {
  const [currentPath, setCurrentPath] = useState<string | undefined>(initialPath)
  const [selectedPath, setSelectedPath] = useState<string | null>(null)
  const [isCreatingFolder, setIsCreatingFolder] = useState(false)
  const [newFolderName, setNewFolderName] = useState('')
  const [createError, setCreateError] = useState<string | null>(null)

  // Fetch directory listing
  const {
    data: directoryData,
    isLoading,
    error,
    refetch,
  } = useQuery({
    queryKey: ['filesystem', 'list', currentPath],
    queryFn: () => api.listDirectory(currentPath),
  })

  // Update selected path when directory changes
  useEffect(() => {
    if (directoryData?.current_path) {
      setSelectedPath(directoryData.current_path)
    }
  }, [directoryData?.current_path])

  const handleNavigate = (path: string) => {
    setCurrentPath(path)
    setSelectedPath(path)
    setIsCreatingFolder(false)
    setNewFolderName('')
    setCreateError(null)
  }

  const handleNavigateUp = () => {
    if (directoryData?.parent_path) {
      handleNavigate(directoryData.parent_path)
    }
  }

  const handleDriveSelect = (drive: DriveInfo) => {
    handleNavigate(`${drive.letter}:/`)
  }

  const handleEntryClick = (entry: DirectoryEntry) => {
    if (entry.is_directory) {
      handleNavigate(entry.path)
    }
  }

  const handleCreateFolder = async () => {
    if (!newFolderName.trim()) {
      setCreateError('Folder name is required')
      return
    }

    // Basic validation
    if (!/^[a-zA-Z0-9_\-. ]+$/.test(newFolderName)) {
      setCreateError('Invalid folder name')
      return
    }

    const newPath = `${directoryData?.current_path}/${newFolderName.trim()}`

    try {
      await api.createDirectory(newPath)
      // Refresh the directory listing
      await refetch()
      // Navigate to the new folder
      handleNavigate(newPath)
    } catch (err) {
      setCreateError(err instanceof Error ? err.message : 'Failed to create folder')
    }
  }

  const handleSelect = () => {
    if (selectedPath) {
      onSelect(selectedPath)
    }
  }

  // Parse breadcrumb segments from path
  const getBreadcrumbs = (path: string): { name: string; path: string }[] => {
    if (!path) return []

    const segments: { name: string; path: string }[] = []

    // Handle Windows drive letters
    if (/^[A-Za-z]:/.test(path)) {
      const drive = path.slice(0, 2)
      segments.push({ name: drive, path: `${drive}/` })
      path = path.slice(3)
    } else if (path.startsWith('/')) {
      segments.push({ name: '/', path: '/' })
      path = path.slice(1)
    }

    // Split remaining path
    const parts = path.split('/').filter(Boolean)
    let currentPath = segments.length > 0 ? segments[0].path : ''

    for (const part of parts) {
      currentPath = currentPath.endsWith('/') ? currentPath + part : currentPath + '/' + part
      segments.push({ name: part, path: currentPath })
    }

    return segments
  }

  const breadcrumbs = directoryData?.current_path ? getBreadcrumbs(directoryData.current_path) : []

  return (

      {/* Header with breadcrumb navigation */}

          <Folder size={20} className="text-[var(--color-neo-progress)]" />
          Select Project Folder

        {/* Breadcrumb navigation */}

          {directoryData?.parent_path && (
            <button
              onClick={handleNavigateUp}
              className="neo-btn neo-btn-ghost p-1"
              title="Go up"
            >
              <ArrowLeft size={16} />
            </button>
          )}

          {breadcrumbs.map((crumb, index) => (

              {index > 0 && <ChevronRight size={14} className="text-gray-400 mx-1" />}
              <button
                onClick={() => handleNavigate(crumb.path)}
                className={`
                  px-2 py-1 rounded text-[#1a1a1a]
                  hover:bg-[var(--color-neo-bg)]
                  ${index === breadcrumbs.length - 1 ? 'font-bold' : ''}
                `}
              >
                {crumb.name}
              </button>

          ))}

      {/* Drive selector (Windows only) */}
      {directoryData?.drives && directoryData.drives.length > 0 && (

            Drives:
            {directoryData.drives.map((drive) => (
              <button
                key={drive.letter}
                onClick={() => handleDriveSelect(drive)}
                className={`
                  neo-btn neo-btn-ghost py-1 px-2 text-sm
                  flex items-center gap-1
                  ${currentPath?.startsWith(drive.letter) ? 'bg-[var(--color-neo-progress)] text-white' : ''}
                `}
              >
                <HardDrive size={14} />
                {drive.letter}: {drive.label && `(${drive.label})`}
              </button>
            ))}

      )}

      {/* Directory listing */}

        {isLoading ? (

            <Loader2 size={24} className="animate-spin text-[var(--color-neo-progress)]" />

        ) : error ? (

            <AlertCircle size={32} className="mx-auto mb-2 text-[var(--color-neo-danger)]" />

              {error instanceof Error ? error.message : 'Failed to load directory'}

            <button onClick={() => refetch()} className="neo-btn neo-btn-ghost mt-2">
              Retry
            </button>

        ) : (

            {/* Directory entries - only show directories */}
            {directoryData?.entries
              .filter((entry) => entry.is_directory)
              .map((entry) => (
                <button
                  key={entry.path}
                  onClick={() => handleEntryClick(entry)}
                  onDoubleClick={() => handleNavigate(entry.path)}
                  className={`
                    w-full text-left p-2 rounded
                    flex items-center gap-2
                    hover:bg-[var(--color-neo-bg)]
                    border-2 border-transparent
                    text-[var(--color-neo-text)]
                    ${selectedPath === entry.path ? 'bg-[var(--color-neo-progress)] bg-opacity-10 border-[var(--color-neo-progress)]' : ''}
                  `}
                >
                  {selectedPath === entry.path ? (
                    <FolderOpen size={18} className="text-[var(--color-neo-progress)] flex-shrink-0" />
                  ) : (
                    <Folder size={18} className="text-[var(--color-neo-pending)] flex-shrink-0" />
                  )}
                  {entry.name}
                  {entry.has_children && (
                    <ChevronRight size={14} className="ml-auto text-gray-400 flex-shrink-0" />
                  )}
                </button>
              ))}

            {/* Empty state */}
            {directoryData?.entries.filter((e) => e.is_directory).length === 0 && (

                <Folder size={32} className="mx-auto mb-2 opacity-50" />

No subfolders

You can create a new folder or select this directory.

            )}

        )}

        {/* New folder creation */}
        {isCreatingFolder && (

              <FolderPlus size={18} className="text-[var(--color-neo-progress)]" />
              <input
                type="text"
                value={newFolderName}
                onChange={(e) => setNewFolderName(e.target.value)}
                placeholder="New folder name"
                className="neo-input flex-1 py-1"
                autoFocus
                onKeyDown={(e) => {
                  if (e.key === 'Enter') handleCreateFolder()
                  if (e.key === 'Escape') {
                    setIsCreatingFolder(false)
                    setNewFolderName('')
                    setCreateError(null)
                  }
                }}
              />
              <button onClick={handleCreateFolder} className="neo-btn neo-btn-primary py-1 px-3">
                Create
              </button>
              <button
                onClick={() => {
                  setIsCreatingFolder(false)
                  setNewFolderName('')
                  setCreateError(null)
                }}
                className="neo-btn neo-btn-ghost py-1 px-2"
              >
                Cancel
              </button>

            {createError && (

{createError}

            )}

        )}

      {/* Footer with selected path and actions */}

        {/* Selected path display */}

Selected path:

{selectedPath || 'No folder selected'}

        {/* Actions */}

          <button
            onClick={() => setIsCreatingFolder(true)}
            className="neo-btn neo-btn-ghost"
            disabled={isCreatingFolder}
          >
            <FolderPlus size={16} />
            New Folder
          </button>

            <button onClick={onCancel} className="neo-btn neo-btn-ghost">
              Cancel
            </button>
            <button
              onClick={handleSelect}
              className="neo-btn neo-btn-primary"
              disabled={!selectedPath}
            >
              Select This Folder
            </button>

  )
}


     1→/**
     2→ * React Query hooks for project data
     3→ */
     4→
     5→import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
     6→import * as api from '../lib/api'
     7→import type { FeatureCreate } from '../lib/types'
     8→
     9→// ============================================================================
    10→// Projects
    11→// ============================================================================
    12→
    13→export function useProjects() {
    14→  return useQuery({
    15→    queryKey: ['projects'],
    16→    queryFn: api.listProjects,
    17→  })
    18→}
    19→
    20→export function useProject(name: string | null) {
    21→  return useQuery({
    22→    queryKey: ['project', name],
    23→    queryFn: () => api.getProject(name!),
    24→    enabled: !!name,
    25→  })
    26→}
    27→
    28→export function useCreateProject() {
    29→  const queryClient = useQueryClient()
    30→
    31→  return useMutation({
    32→    mutationFn: ({ name, path, specMethod }: { name: string; path: string; specMethod?: 'claude' | 'manual' }) =>
    33→      api.createProject(name, path, specMethod),
    34→    onSuccess: () => {
    35→      queryClient.invalidateQueries({ queryKey: ['projects'] })
    36→    },
    37→  })
    38→}
    39→
    40→export function useDeleteProject() {
    41→  const queryClient = useQueryClient()
    42→
    43→  return useMutation({
    44→    mutationFn: (name: string) => api.deleteProject(name),
    45→    onSuccess: () => {
    46→      queryClient.invalidateQueries({ queryKey: ['projects'] })
    47→    },
    48→  })
    49→}
    50→
    51→// ============================================================================
    52→// Features
    53→// ============================================================================
    54→
    55→export function useFeatures(projectName: string | null) {
    56→  return useQuery({
    57→    queryKey: ['features', projectName],
    58→    queryFn: () => api.listFeatures(projectName!),
    59→    enabled: !!projectName,
    60→    refetchInterval: 5000, // Refetch every 5 seconds for real-time updates
    61→  })
    62→}
    63→
    64→export function useCreateFeature(projectName: string) {
    65→  const queryClient = useQueryClient()
    66→
    67→  return useMutation({
    68→    mutationFn: (feature: FeatureCreate) => api.createFeature(projectName, feature),
    69→    onSuccess: () => {
    70→      queryClient.invalidateQueries({ queryKey: ['features', projectName] })
    71→    },
    72→  })
    73→}
    74→
    75→export function useDeleteFeature(projectName: string) {
    76→  const queryClient = useQueryClient()
    77→
    78→  return useMutation({
    79→    mutationFn: (featureId: number) => api.deleteFeature(projectName, featureId),
    80→    onSuccess: () => {
    81→      queryClient.invalidateQueries({ queryKey: ['features', projectName] })
    82→    },
    83→  })
    84→}
    85→
    86→export function useSkipFeature(projectName: string) {
    87→  const queryClient = useQueryClient()
    88→
    89→  return useMutation({
    90→    mutationFn: (featureId: number) => api.skipFeature(projectName, featureId),
    91→    onSuccess: () => {
    92→      queryClient.invalidateQueries({ queryKey: ['features', projectName] })
    93→    },
    94→  })
    95→}
    96→
    97→// ============================================================================
    98→// Agent
    99→// ============================================================================
   100→
   101→export function useAgentStatus(projectName: string | null) {
   102→  return useQuery({
   103→    queryKey: ['agent-status', projectName],
   104→    queryFn: () => api.getAgentStatus(projectName!),
   105→    enabled: !!projectName,
   106→    refetchInterval: 3000, // Poll every 3 seconds
   107→  })
   108→}
   109→
   110→export function useStartAgent(projectName: string) {
   111→  const queryClient = useQueryClient()
   112→
   113→  return useMutation({
   114→    mutationFn: (yoloMode: boolean = false) => api.startAgent(projectName, yoloMode),
   115→    onSuccess: () => {
   116→      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
   117→    },
   118→  })
   119→}
   120→
   121→export function useStopAgent(projectName: string) {
   122→  const queryClient = useQueryClient()
   123→
   124→  return useMutation({
   125→    mutationFn: () => api.stopAgent(projectName),
   126→    onSuccess: () => {
   127→      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
   128→    },
   129→  })
   130→}
   131→
   132→export function usePauseAgent(projectName: string) {
   133→  const queryClient = useQueryClient()
   134→
   135→  return useMutation({
   136→    mutationFn: () => api.pauseAgent(projectName),
   137→    onSuccess: () => {
   138→      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
   139→    },
   140→  })
   141→}
   142→
   143→export function useResumeAgent(projectName: string) {
   144→  const queryClient = useQueryClient()
   145→
   146→  return useMutation({
   147→    mutationFn: () => api.resumeAgent(projectName),
   148→    onSuccess: () => {
   149→      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
   150→    },
   151→  })
   152→}
   153→
   154→// ============================================================================
   155→// Setup
   156→// ============================================================================
   157→
   158→export function useSetupStatus() {
   159→  return useQuery({
   160→    queryKey: ['setup-status'],
   161→    queryFn: api.getSetupStatus,
   162→    staleTime: 60000, // Cache for 1 minute
   163→  })
   164→}
   165→
   166→export function useHealthCheck() {
   167→  return useQuery({
   168→    queryKey: ['health'],
   169→    queryFn: api.healthCheck,
   170→    retry: false,
   171→  })
   172→}
   173→
   174→// ============================================================================
   175→// Filesystem
   176→// ============================================================================
   177→
   178→export function useListDirectory(path?: string) {
   179→  return useQuery({
   180→    queryKey: ['filesystem', 'list', path],
   181→    queryFn: () => api.listDirectory(path),
   182→  })
   183→}
   184→
   185→export function useCreateDirectory() {
   186→  const queryClient = useQueryClient()
   187→
   188→  return useMutation({
   189→    mutationFn: (path: string) => api.createDirectory(path),
   190→    onSuccess: (_, path) => {
   191→      // Invalidate parent directory listing
   192→      const parentPath = path.split('/').slice(0, -1).join('/') || undefined
   193→      queryClient.invalidateQueries({ queryKey: ['filesystem', 'list', parentPath] })
   194→    },
   195→  })
   196→}
   197→
   198→export function useValidatePath() {
   199→  return useMutation({
   200→    mutationFn: (path: string) => api.validatePath(path),
   201→  })
   202→}
   203→

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

/**
 * React Query hooks for project data
 */

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import * as api from '../lib/api'
import type { FeatureCreate } from '../lib/types'

// ============================================================================
// Projects
// ============================================================================

export function useProjects() {
  return useQuery({
    queryKey: ['projects'],
    queryFn: api.listProjects,
  })
}

export function useProject(name: string | null) {
  return useQuery({
    queryKey: ['project', name],
    queryFn: () => api.getProject(name!),
    enabled: !!name,
  })
}

export function useCreateProject() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: ({ name, path, specMethod }: { name: string; path: string; specMethod?: 'claude' | 'manual' }) =>
      api.createProject(name, path, specMethod),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['projects'] })
    },
  })
}

export function useDeleteProject() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (name: string) => api.deleteProject(name),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['projects'] })
    },
  })
}

// ============================================================================
// Features
// ============================================================================

export function useFeatures(projectName: string | null) {
  return useQuery({
    queryKey: ['features', projectName],
    queryFn: () => api.listFeatures(projectName!),
    enabled: !!projectName,
    refetchInterval: 5000, // Refetch every 5 seconds for real-time updates
  })
}

export function useCreateFeature(projectName: string) {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (feature: FeatureCreate) => api.createFeature(projectName, feature),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['features', projectName] })
    },
  })
}

export function useDeleteFeature(projectName: string) {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (featureId: number) => api.deleteFeature(projectName, featureId),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['features', projectName] })
    },
  })
}

export function useSkipFeature(projectName: string) {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (featureId: number) => api.skipFeature(projectName, featureId),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['features', projectName] })
    },
  })
}

// ============================================================================
// Agent
// ============================================================================

export function useAgentStatus(projectName: string | null) {
  return useQuery({
    queryKey: ['agent-status', projectName],
    queryFn: () => api.getAgentStatus(projectName!),
    enabled: !!projectName,
    refetchInterval: 3000, // Poll every 3 seconds
  })
}

export function useStartAgent(projectName: string) {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (yoloMode: boolean = false) => api.startAgent(projectName, yoloMode),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
    },
  })
}

export function useStopAgent(projectName: string) {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () => api.stopAgent(projectName),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
    },
  })
}

export function usePauseAgent(projectName: string) {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () => api.pauseAgent(projectName),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
    },
  })
}

export function useResumeAgent(projectName: string) {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () => api.resumeAgent(projectName),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
    },
  })
}

// ============================================================================
// Setup
// ============================================================================

export function useSetupStatus() {
  return useQuery({
    queryKey: ['setup-status'],
    queryFn: api.getSetupStatus,
    staleTime: 60000, // Cache for 1 minute
  })
}

export function useHealthCheck() {
  return useQuery({
    queryKey: ['health'],
    queryFn: api.healthCheck,
    retry: false,
  })
}

// ============================================================================
// Filesystem
// ============================================================================

export function useListDirectory(path?: string) {
  return useQuery({
    queryKey: ['filesystem', 'list', path],
    queryFn: () => api.listDirectory(path),
  })
}

export function useCreateDirectory() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (path: string) => api.createDirectory(path),
    onSuccess: (_, path) => {
      // Invalidate parent directory listing
      const parentPath = path.split('/').slice(0, -1).join('/') || undefined
      queryClient.invalidateQueries({ queryKey: ['filesystem', 'list', parentPath] })
    },
  })
}

export function useValidatePath() {
  return useMutation({
    mutationFn: (path: string) => api.validatePath(path),
  })
}


     1→/**
     2→ * API Client for the Autonomous Coding UI
     3→ */
     4→
     5→import type {
     6→  ProjectSummary,
     7→  ProjectDetail,
     8→  ProjectPrompts,
     9→  FeatureListResponse,
    10→  Feature,
    11→  FeatureCreate,
    12→  AgentStatusResponse,
    13→  AgentActionResponse,
    14→  SetupStatus,
    15→  DirectoryListResponse,
    16→  PathValidationResponse,
    17→  AssistantConversation,
    18→  AssistantConversationDetail,
    19→} from './types'
    20→
    21→const API_BASE = '/api'
    22→
    23→async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
    24→  const response = await fetch(`${API_BASE}${url}`, {
    25→    ...options,
    26→    headers: {
    27→      'Content-Type': 'application/json',
    28→      ...options?.headers,
    29→    },
    30→  })
    31→
    32→  if (!response.ok) {
    33→    const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
    34→    throw new Error(error.detail || `HTTP ${response.status}`)
    35→  }
    36→
    37→  return response.json()
    38→}
    39→
    40→// ============================================================================
    41→// Projects API
    42→// ============================================================================
    43→
    44→export async function listProjects(): Promise<ProjectSummary[]> {
    45→  return fetchJSON('/projects')
    46→}
    47→
    48→export async function createProject(
    49→  name: string,
    50→  path: string,
    51→  specMethod: 'claude' | 'manual' = 'manual'
    52→): Promise<ProjectSummary> {
    53→  return fetchJSON('/projects', {
    54→    method: 'POST',
    55→    body: JSON.stringify({ name, path, spec_method: specMethod }),
    56→  })
    57→}
    58→
    59→export async function getProject(name: string): Promise<ProjectDetail> {
    60→  return fetchJSON(`/projects/${encodeURIComponent(name)}`)
    61→}
    62→
    63→export async function deleteProject(name: string): Promise<void> {
    64→  await fetchJSON(`/projects/${encodeURIComponent(name)}`, {
    65→    method: 'DELETE',
    66→  })
    67→}
    68→
    69→export async function getProjectPrompts(name: string): Promise<ProjectPrompts> {
    70→  return fetchJSON(`/projects/${encodeURIComponent(name)}/prompts`)
    71→}
    72→
    73→export async function updateProjectPrompts(
    74→  name: string,
    75→  prompts: Partial<ProjectPrompts>
    76→): Promise<void> {
    77→  await fetchJSON(`/projects/${encodeURIComponent(name)}/prompts`, {
    78→    method: 'PUT',
    79→    body: JSON.stringify(prompts),
    80→  })
    81→}
    82→
    83→// ============================================================================
    84→// Features API
    85→// ============================================================================
    86→
    87→export async function listFeatures(projectName: string): Promise<FeatureListResponse> {
    88→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features`)
    89→}
    90→
    91→export async function createFeature(projectName: string, feature: FeatureCreate): Promise<Feature> {
    92→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features`, {
    93→    method: 'POST',
    94→    body: JSON.stringify(feature),
    95→  })
    96→}
    97→
    98→export async function getFeature(projectName: string, featureId: number): Promise<Feature> {
    99→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}`)
   100→}
   101→
   102→export async function deleteFeature(projectName: string, featureId: number): Promise<void> {
   103→  await fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}`, {
   104→    method: 'DELETE',
   105→  })
   106→}
   107→
   108→export async function skipFeature(projectName: string, featureId: number): Promise<void> {
   109→  await fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}/skip`, {
   110→    method: 'PATCH',
   111→  })
   112→}
   113→
   114→// ============================================================================
   115→// Agent API
   116→// ============================================================================
   117→
   118→export async function getAgentStatus(projectName: string): Promise<AgentStatusResponse> {
   119→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/status`)
   120→}
   121→
   122→export async function startAgent(
   123→  projectName: string,
   124→  yoloMode: boolean = false
   125→): Promise<AgentActionResponse> {
   126→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/start`, {
   127→    method: 'POST',
   128→    body: JSON.stringify({ yolo_mode: yoloMode }),
   129→  })
   130→}
   131→
   132→export async function stopAgent(projectName: string): Promise<AgentActionResponse> {
   133→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/stop`, {
   134→    method: 'POST',
   135→  })
   136→}
   137→
   138→export async function pauseAgent(projectName: string): Promise<AgentActionResponse> {
   139→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/pause`, {
   140→    method: 'POST',
   141→  })
   142→}
   143→
   144→export async function resumeAgent(projectName: string): Promise<AgentActionResponse> {
   145→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/resume`, {
   146→    method: 'POST',
   147→  })
   148→}
   149→
   150→// ============================================================================
   151→// Spec Creation API
   152→// ============================================================================
   153→
   154→export interface SpecFileStatus {
   155→  exists: boolean
   156→  status: 'complete' | 'in_progress' | 'not_started' | 'error' | 'unknown'
   157→  feature_count: number | null
   158→  timestamp: string | null
   159→  files_written: string[]
   160→}
   161→
   162→export async function getSpecStatus(projectName: string): Promise<SpecFileStatus> {
   163→  return fetchJSON(`/spec/status/${encodeURIComponent(projectName)}`)
   164→}
   165→
   166→// ============================================================================
   167→// Setup API
   168→// ============================================================================
   169→
   170→export async function getSetupStatus(): Promise<SetupStatus> {
   171→  return fetchJSON('/setup/status')
   172→}
   173→
   174→export async function healthCheck(): Promise<{ status: string }> {
   175→  return fetchJSON('/health')
   176→}
   177→
   178→// ============================================================================
   179→// Filesystem API
   180→// ============================================================================
   181→
   182→export async function listDirectory(path?: string): Promise<DirectoryListResponse> {
   183→  const params = path ? `?path=${encodeURIComponent(path)}` : ''
   184→  return fetchJSON(`/filesystem/list${params}`)
   185→}
   186→
   187→export async function createDirectory(fullPath: string): Promise<{ success: boolean; path: string }> {
   188→  // Backend expects { parent_path, name }, not { path }
   189→  // Split the full path into parent directory and folder name
   190→
   191→  // Remove trailing slash if present
   192→  const normalizedPath = fullPath.endsWith('/') ? fullPath.slice(0, -1) : fullPath
   193→
   194→  // Find the last path separator
   195→  const lastSlash = normalizedPath.lastIndexOf('/')
   196→
   197→  let parentPath: string
   198→  let name: string
   199→
   200→  // Handle Windows drive root (e.g., "C:/newfolder")
   201→  if (lastSlash === 2 && /^[A-Za-z]:/.test(normalizedPath)) {
   202→    // Path like "C:/newfolder" - parent is "C:/"
   203→    parentPath = normalizedPath.substring(0, 3) // "C:/"
   204→    name = normalizedPath.substring(3)
   205→  } else if (lastSlash > 0) {
   206→    parentPath = normalizedPath.substring(0, lastSlash)
   207→    name = normalizedPath.substring(lastSlash + 1)
   208→  } else if (lastSlash === 0) {
   209→    // Unix root path like "/newfolder"
   210→    parentPath = '/'
   211→    name = normalizedPath.substring(1)
   212→  } else {
   213→    // No slash - invalid path
   214→    throw new Error('Invalid path: must be an absolute path')
   215→  }
   216→
   217→  if (!name) {
   218→    throw new Error('Invalid path: directory name is empty')
   219→  }
   220→
   221→  return fetchJSON('/filesystem/create-directory', {
   222→    method: 'POST',
   223→    body: JSON.stringify({ parent_path: parentPath, name }),
   224→  })
   225→}
   226→
   227→export async function validatePath(path: string): Promise<PathValidationResponse> {
   228→  return fetchJSON('/filesystem/validate', {
   229→    method: 'POST',
   230→    body: JSON.stringify({ path }),
   231→  })
   232→}
   233→
   234→// ============================================================================
   235→// Assistant Chat API
   236→// ============================================================================
   237→
   238→export async function listAssistantConversations(
   239→  projectName: string
   240→): Promise<AssistantConversation[]> {
   241→  return fetchJSON(`/assistant/conversations/${encodeURIComponent(projectName)}`)
   242→}
   243→
   244→export async function getAssistantConversation(
   245→  projectName: string,
   246→  conversationId: number
   247→): Promise<AssistantConversationDetail> {
   248→  return fetchJSON(
   249→    `/assistant/conversations/${encodeURIComponent(projectName)}/${conversationId}`
   250→  )
   251→}
   252→
   253→export async function createAssistantConversation(
   254→  projectName: string
   255→): Promise<AssistantConversation> {
   256→  return fetchJSON(`/assistant/conversations/${encodeURIComponent(projectName)}`, {
   257→    method: 'POST',
   258→  })
   259→}
   260→
   261→export async function deleteAssistantConversation(
   262→  projectName: string,
   263→  conversationId: number
   264→): Promise<void> {
   265→  await fetchJSON(
   266→    `/assistant/conversations/${encodeURIComponent(projectName)}/${conversationId}`,
   267→    { method: 'DELETE' }
   268→  )
   269→}
   270→

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

/**
 * API Client for the Autonomous Coding UI
 */

import type {
  ProjectSummary,
  ProjectDetail,
  ProjectPrompts,
  FeatureListResponse,
  Feature,
  FeatureCreate,
  AgentStatusResponse,
  AgentActionResponse,
  SetupStatus,
  DirectoryListResponse,
  PathValidationResponse,
  AssistantConversation,
  AssistantConversationDetail,
} from './types'

const API_BASE = '/api'

async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(`${API_BASE}${url}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options?.headers,
    },
  })

  if (!response.ok) {
    const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
    throw new Error(error.detail || `HTTP ${response.status}`)
  }

  return response.json()
}

// ============================================================================
// Projects API
// ============================================================================

export async function listProjects(): Promise<ProjectSummary[]> {
  return fetchJSON('/projects')
}

export async function createProject(
  name: string,
  path: string,
  specMethod: 'claude' | 'manual' = 'manual'
): Promise<ProjectSummary> {
  return fetchJSON('/projects', {
    method: 'POST',
    body: JSON.stringify({ name, path, spec_method: specMethod }),
  })
}

export async function getProject(name: string): Promise<ProjectDetail> {
  return fetchJSON(`/projects/${encodeURIComponent(name)}`)
}

export async function deleteProject(name: string): Promise<void> {
  await fetchJSON(`/projects/${encodeURIComponent(name)}`, {
    method: 'DELETE',
  })
}

export async function getProjectPrompts(name: string): Promise<ProjectPrompts> {
  return fetchJSON(`/projects/${encodeURIComponent(name)}/prompts`)
}

export async function updateProjectPrompts(
  name: string,
  prompts: Partial<ProjectPrompts>
): Promise<void> {
  await fetchJSON(`/projects/${encodeURIComponent(name)}/prompts`, {
    method: 'PUT',
    body: JSON.stringify(prompts),
  })
}

// ============================================================================
// Features API
// ============================================================================

export async function listFeatures(projectName: string): Promise<FeatureListResponse> {
  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features`)
}

export async function createFeature(projectName: string, feature: FeatureCreate): Promise<Feature> {
  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features`, {
    method: 'POST',
    body: JSON.stringify(feature),
  })
}

export async function getFeature(projectName: string, featureId: number): Promise<Feature> {
  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}`)
}

export async function deleteFeature(projectName: string, featureId: number): Promise<void> {
  await fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}`, {
    method: 'DELETE',
  })
}

export async function skipFeature(projectName: string, featureId: number): Promise<void> {
  await fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}/skip`, {
    method: 'PATCH',
  })
}

// ============================================================================
// Agent API
// ============================================================================

export async function getAgentStatus(projectName: string): Promise<AgentStatusResponse> {
  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/status`)
}

export async function startAgent(
  projectName: string,
  yoloMode: boolean = false
): Promise<AgentActionResponse> {
  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/start`, {
    method: 'POST',
    body: JSON.stringify({ yolo_mode: yoloMode }),
  })
}

export async function stopAgent(projectName: string): Promise<AgentActionResponse> {
  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/stop`, {
    method: 'POST',
  })
}

export async function pauseAgent(projectName: string): Promise<AgentActionResponse> {
  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/pause`, {
    method: 'POST',
  })
}

export async function resumeAgent(projectName: string): Promise<AgentActionResponse> {
  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/resume`, {
    method: 'POST',
  })
}

// ============================================================================
// Spec Creation API
// ============================================================================

export interface SpecFileStatus {
  exists: boolean
  status: 'complete' | 'in_progress' | 'not_started' | 'error' | 'unknown'
  feature_count: number | null
  timestamp: string | null
  files_written: string[]
}

export async function getSpecStatus(projectName: string): Promise<SpecFileStatus> {
  return fetchJSON(`/spec/status/${encodeURIComponent(projectName)}`)
}

// ============================================================================
// Setup API
// ============================================================================

export async function getSetupStatus(): Promise<SetupStatus> {
  return fetchJSON('/setup/status')
}

export async function healthCheck(): Promise<{ status: string }> {
  return fetchJSON('/health')
}

// ============================================================================
// Filesystem API
// ============================================================================

export async function listDirectory(path?: string): Promise<DirectoryListResponse> {
  const params = path ? `?path=${encodeURIComponent(path)}` : ''
  return fetchJSON(`/filesystem/list${params}`)
}

export async function createDirectory(fullPath: string): Promise<{ success: boolean; path: string }> {
  // Backend expects { parent_path, name }, not { path }
  // Split the full path into parent directory and folder name

  // Remove trailing slash if present
  const normalizedPath = fullPath.endsWith('/') ? fullPath.slice(0, -1) : fullPath

  // Find the last path separator
  const lastSlash = normalizedPath.lastIndexOf('/')

  let parentPath: string
  let name: string

  // Handle Windows drive root (e.g., "C:/newfolder")
  if (lastSlash === 2 && /^[A-Za-z]:/.test(normalizedPath)) {
    // Path like "C:/newfolder" - parent is "C:/"
    parentPath = normalizedPath.substring(0, 3) // "C:/"
    name = normalizedPath.substring(3)
  } else if (lastSlash > 0) {
    parentPath = normalizedPath.substring(0, lastSlash)
    name = normalizedPath.substring(lastSlash + 1)
  } else if (lastSlash === 0) {
    // Unix root path like "/newfolder"
    parentPath = '/'
    name = normalizedPath.substring(1)
  } else {
    // No slash - invalid path
    throw new Error('Invalid path: must be an absolute path')
  }

  if (!name) {
    throw new Error('Invalid path: directory name is empty')
  }

  return fetchJSON('/filesystem/create-directory', {
    method: 'POST',
    body: JSON.stringify({ parent_path: parentPath, name }),
  })
}

export async function validatePath(path: string): Promise<PathValidationResponse> {
  return fetchJSON('/filesystem/validate', {
    method: 'POST',
    body: JSON.stringify({ path }),
  })
}

// ============================================================================
// Assistant Chat API
// ============================================================================

export async function listAssistantConversations(
  projectName: string
): Promise<AssistantConversation[]> {
  return fetchJSON(`/assistant/conversations/${encodeURIComponent(projectName)}`)
}

export async function getAssistantConversation(
  projectName: string,
  conversationId: number
): Promise<AssistantConversationDetail> {
  return fetchJSON(
    `/assistant/conversations/${encodeURIComponent(projectName)}/${conversationId}`
  )
}

export async function createAssistantConversation(
  projectName: string
): Promise<AssistantConversation> {
  return fetchJSON(`/assistant/conversations/${encodeURIComponent(projectName)}`, {
    method: 'POST',
  })
}

export async function deleteAssistantConversation(
  projectName: string,
  conversationId: number
): Promise<void> {
  await fetchJSON(
    `/assistant/conversations/${encodeURIComponent(projectName)}/${conversationId}`,
    { method: 'DELETE' }
  )
}


Perfect! Now let me search for any type definitions and check for existing import patterns to complete my understanding.


6:export interface ProjectStats {
13:export interface ProjectSummary {
20:export interface ProjectDetail extends ProjectSummary {
31:export interface DirectoryEntry {
38:export interface DirectoryListResponse {
53:export interface ProjectPrompts {

[Showing results with pagination = limit: 50, offset: 0]


Let me read the types file to get the complete picture:


     1→/**
     2→ * TypeScript types for the Autonomous Coding UI
     3→ */
     4→
     5→// Project types
     6→export interface ProjectStats {
     7→  passing: number
     8→  in_progress: number
     9→  total: number
    10→  percentage: number
    11→}
    12→
    13→export interface ProjectSummary {
    14→  name: string
    15→  path: string
    16→  has_spec: boolean
    17→  stats: ProjectStats
    18→}
    19→
    20→export interface ProjectDetail extends ProjectSummary {
    21→  prompts_dir: string
    22→}
    23→
    24→// Filesystem types
    25→export interface DriveInfo {
    26→  letter: string
    27→  label: string
    28→  available?: boolean
    29→}
    30→
    31→export interface DirectoryEntry {
    32→  name: string
    33→  path: string
    34→  is_directory: boolean
    35→  has_children: boolean
    36→}
    37→
    38→export interface DirectoryListResponse {
    39→  current_path: string
    40→  parent_path: string | null
    41→  entries: DirectoryEntry[]
    42→  drives: DriveInfo[] | null
    43→}
    44→
    45→export interface PathValidationResponse {
    46→  valid: boolean
    47→  exists: boolean
    48→  is_directory: boolean
    49→  can_write: boolean
    50→  message: string
    51→}
    52→
    53→export interface ProjectPrompts {
    54→  app_spec: string
    55→  initializer_prompt: string
    56→  coding_prompt: string
    57→}
    58→
    59→// Feature types
    60→export interface Feature {
    61→  id: number
    62→  priority: number
    63→  category: string
    64→  name: string
    65→  description: string
    66→  steps: string[]
    67→  passes: boolean
    68→  in_progress: boolean
    69→}
    70→
    71→export interface FeatureListResponse {
    72→  pending: Feature[]
    73→  in_progress: Feature[]
    74→  done: Feature[]
    75→}
    76→
    77→export interface FeatureCreate {
    78→  category: string
    79→  name: string
    80→  description: string
    81→  steps: string[]
    82→  priority?: number
    83→}
    84→
    85→// Agent types
    86→export type AgentStatus = 'stopped' | 'running' | 'paused' | 'crashed'
    87→
    88→export interface AgentStatusResponse {
    89→  status: AgentStatus
    90→  pid: number | null
    91→  started_at: string | null
    92→  yolo_mode: boolean
    93→}
    94→
    95→export interface AgentActionResponse {
    96→  success: boolean
    97→  status: AgentStatus
    98→  message: string
    99→}
   100→
   101→// Setup types
   102→export interface SetupStatus {
   103→  claude_cli: boolean
   104→  credentials: boolean
   105→  node: boolean
   106→  npm: boolean
   107→}
   108→
   109→// WebSocket message types
   110→export type WSMessageType = 'progress' | 'feature_update' | 'log' | 'agent_status' | 'pong'
   111→
   112→export interface WSProgressMessage {
   113→  type: 'progress'
   114→  passing: number
   115→  in_progress: number
   116→  total: number
   117→  percentage: number
   118→}
   119→
   120→export interface WSFeatureUpdateMessage {
   121→  type: 'feature_update'
   122→  feature_id: number
   123→  passes: boolean
   124→}
   125→
   126→export interface WSLogMessage {
   127→  type: 'log'
   128→  line: string
   129→  timestamp: string
   130→}
   131→
   132→export interface WSAgentStatusMessage {
   133→  type: 'agent_status'
   134→  status: AgentStatus
   135→}
   136→
   137→export interface WSPongMessage {
   138→  type: 'pong'
   139→}
   140→
   141→export type WSMessage =
   142→  | WSProgressMessage
   143→  | WSFeatureUpdateMessage
   144→  | WSLogMessage
   145→  | WSAgentStatusMessage
   146→  | WSPongMessage
   147→
   148→// ============================================================================
   149→// Spec Chat Types
   150→// ============================================================================
   151→
   152→export interface SpecQuestionOption {
   153→  label: string
   154→  description: string
   155→}
   156→
   157→export interface SpecQuestion {
   158→  question: string
   159→  header: string
   160→  options: SpecQuestionOption[]
   161→  multiSelect: boolean
   162→}
   163→
   164→export interface SpecChatTextMessage {
   165→  type: 'text'
   166→  content: string
   167→}
   168→
   169→export interface SpecChatQuestionMessage {
   170→  type: 'question'
   171→  questions: SpecQuestion[]
   172→  tool_id?: string
   173→}
   174→
   175→export interface SpecChatCompleteMessage {
   176→  type: 'spec_complete'
   177→  path: string
   178→}
   179→
   180→export interface SpecChatFileWrittenMessage {
   181→  type: 'file_written'
   182→  path: string
   183→}
   184→
   185→export interface SpecChatSessionCompleteMessage {
   186→  type: 'complete'
   187→}
   188→
   189→export interface SpecChatErrorMessage {
   190→  type: 'error'
   191→  content: string
   192→}
   193→
   194→export interface SpecChatPongMessage {
   195→  type: 'pong'
   196→}
   197→
   198→export interface SpecChatResponseDoneMessage {
   199→  type: 'response_done'
   200→}
   201→
   202→export type SpecChatServerMessage =
   203→  | SpecChatTextMessage
   204→  | SpecChatQuestionMessage
   205→  | SpecChatCompleteMessage
   206→  | SpecChatFileWrittenMessage
   207→  | SpecChatSessionCompleteMessage
   208→  | SpecChatErrorMessage
   209→  | SpecChatPongMessage
   210→  | SpecChatResponseDoneMessage
   211→
   212→// Image attachment for chat messages
   213→export interface ImageAttachment {
   214→  id: string
   215→  filename: string
   216→  mimeType: 'image/jpeg' | 'image/png'
   217→  base64Data: string    // Raw base64 (without data: prefix)
   218→  previewUrl: string    // data: URL for display
   219→  size: number          // File size in bytes
   220→}
   221→
   222→// UI chat message for display
   223→export interface ChatMessage {
   224→  id: string
   225→  role: 'user' | 'assistant' | 'system'
   226→  content: string
   227→  attachments?: ImageAttachment[]
   228→  timestamp: Date
   229→  questions?: SpecQuestion[]
   230→  isStreaming?: boolean
   231→}
   232→
   233→// ============================================================================
   234→// Assistant Chat Types
   235→// ============================================================================
   236→
   237→export interface AssistantConversation {
   238→  id: number
   239→  project_name: string
   240→  title: string | null
   241→  created_at: string | null
   242→  updated_at: string | null
   243→  message_count: number
   244→}
   245→
   246→export interface AssistantMessage {
   247→  id: number
   248→  role: 'user' | 'assistant' | 'system'
   249→  content: string
   250→  timestamp: string | null
   251→}
   252→
   253→export interface AssistantConversationDetail {
   254→  id: number
   255→  project_name: string
   256→  title: string | null
   257→  created_at: string | null
   258→  updated_at: string | null
   259→  messages: AssistantMessage[]
   260→}
   261→
   262→export interface AssistantChatTextMessage {
   263→  type: 'text'
   264→  content: string
   265→}
   266→
   267→export interface AssistantChatToolCallMessage {
   268→  type: 'tool_call'
   269→  tool: string
   270→  input: Record<string, unknown>
   271→}
   272→
   273→export interface AssistantChatResponseDoneMessage {
   274→  type: 'response_done'
   275→}
   276→
   277→export interface AssistantChatErrorMessage {
   278→  type: 'error'
   279→  content: string
   280→}
   281→
   282→export interface AssistantChatConversationCreatedMessage {
   283→  type: 'conversation_created'
   284→  conversation_id: number
   285→}
   286→
   287→export interface AssistantChatPongMessage {
   288→  type: 'pong'
   289→}
   290→
   291→export type AssistantChatServerMessage =
   292→  | AssistantChatTextMessage
   293→  | AssistantChatToolCallMessage
   294→  | AssistantChatResponseDoneMessage
   295→  | AssistantChatErrorMessage
   296→  | AssistantChatConversationCreatedMessage
   297→  | AssistantChatPongMessage
   298→

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

/**
 * TypeScript types for the Autonomous Coding UI
 */

// Project types
export interface ProjectStats {
  passing: number
  in_progress: number
  total: number
  percentage: number
}

export interface ProjectSummary {
  name: string
  path: string
  has_spec: boolean
  stats: ProjectStats
}

export interface ProjectDetail extends ProjectSummary {
  prompts_dir: string
}

// Filesystem types
export interface DriveInfo {
  letter: string
  label: string
  available?: boolean
}

export interface DirectoryEntry {
  name: string
  path: string
  is_directory: boolean
  has_children: boolean
}

export interface DirectoryListResponse {
  current_path: string
  parent_path: string | null
  entries: DirectoryEntry[]
  drives: DriveInfo[] | null
}

export interface PathValidationResponse {
  valid: boolean
  exists: boolean
  is_directory: boolean
  can_write: boolean
  message: string
}

export interface ProjectPrompts {
  app_spec: string
  initializer_prompt: string
  coding_prompt: string
}

// Feature types
export interface Feature {
  id: number
  priority: number
  category: string
  name: string
  description: string
  steps: string[]
  passes: boolean
  in_progress: boolean
}

export interface FeatureListResponse {
  pending: Feature[]
  in_progress: Feature[]
  done: Feature[]
}

export interface FeatureCreate {
  category: string
  name: string
  description: string
  steps: string[]
  priority?: number
}

// Agent types
export type AgentStatus = 'stopped' | 'running' | 'paused' | 'crashed'

export interface AgentStatusResponse {
  status: AgentStatus
  pid: number | null
  started_at: string | null
  yolo_mode: boolean
}

export interface AgentActionResponse {
  success: boolean
  status: AgentStatus
  message: string
}

// Setup types
export interface SetupStatus {
  claude_cli: boolean
  credentials: boolean
  node: boolean
  npm: boolean
}

// WebSocket message types
export type WSMessageType = 'progress' | 'feature_update' | 'log' | 'agent_status' | 'pong'

export interface WSProgressMessage {
  type: 'progress'
  passing: number
  in_progress: number
  total: number
  percentage: number
}

export interface WSFeatureUpdateMessage {
  type: 'feature_update'
  feature_id: number
  passes: boolean
}

export interface WSLogMessage {
  type: 'log'
  line: string
  timestamp: string
}

export interface WSAgentStatusMessage {
  type: 'agent_status'
  status: AgentStatus
}

export interface WSPongMessage {
  type: 'pong'
}

export type WSMessage =
  | WSProgressMessage
  | WSFeatureUpdateMessage
  | WSLogMessage
  | WSAgentStatusMessage
  | WSPongMessage

// ============================================================================
// Spec Chat Types
// ============================================================================

export interface SpecQuestionOption {
  label: string
  description: string
}

export interface SpecQuestion {
  question: string
  header: string
  options: SpecQuestionOption[]
  multiSelect: boolean
}

export interface SpecChatTextMessage {
  type: 'text'
  content: string
}

export interface SpecChatQuestionMessage {
  type: 'question'
  questions: SpecQuestion[]
  tool_id?: string
}

export interface SpecChatCompleteMessage {
  type: 'spec_complete'
  path: string
}

export interface SpecChatFileWrittenMessage {
  type: 'file_written'
  path: string
}

export interface SpecChatSessionCompleteMessage {
  type: 'complete'
}

export interface SpecChatErrorMessage {
  type: 'error'
  content: string
}

export interface SpecChatPongMessage {
  type: 'pong'
}

export interface SpecChatResponseDoneMessage {
  type: 'response_done'
}

export type SpecChatServerMessage =
  | SpecChatTextMessage
  | SpecChatQuestionMessage
  | SpecChatCompleteMessage
  | SpecChatFileWrittenMessage
  | SpecChatSessionCompleteMessage
  | SpecChatErrorMessage
  | SpecChatPongMessage
  | SpecChatResponseDoneMessage

// Image attachment for chat messages
export interface ImageAttachment {
  id: string
  filename: string
  mimeType: 'image/jpeg' | 'image/png'
  base64Data: string    // Raw base64 (without data: prefix)
  previewUrl: string    // data: URL for display
  size: number          // File size in bytes
}

// UI chat message for display
export interface ChatMessage {
  id: string
  role: 'user' | 'assistant' | 'system'
  content: string
  attachments?: ImageAttachment[]
  timestamp: Date
  questions?: SpecQuestion[]
  isStreaming?: boolean
}

// ============================================================================
// Assistant Chat Types
// ============================================================================

export interface AssistantConversation {
  id: number
  project_name: string
  title: string | null
  created_at: string | null
  updated_at: string | null
  message_count: number
}

export interface AssistantMessage {
  id: number
  role: 'user' | 'assistant' | 'system'
  content: string
  timestamp: string | null
}

export interface AssistantConversationDetail {
  id: number
  project_name: string
  title: string | null
  created_at: string | null
  updated_at: string | null
  messages: AssistantMessage[]
}

export interface AssistantChatTextMessage {
  type: 'text'
  content: string
}

export interface AssistantChatToolCallMessage {
  type: 'tool_call'
  tool: string
  input: Record<string, unknown>
}

export interface AssistantChatResponseDoneMessage {
  type: 'response_done'
}

export interface AssistantChatErrorMessage {
  type: 'error'
  content: string
}

export interface AssistantChatConversationCreatedMessage {
  type: 'conversation_created'
  conversation_id: number
}

export interface AssistantChatPongMessage {
  type: 'pong'
}

export type AssistantChatServerMessage =
  | AssistantChatTextMessage
  | AssistantChatToolCallMessage
  | AssistantChatResponseDoneMessage
  | AssistantChatErrorMessage
  | AssistantChatConversationCreatedMessage
  | AssistantChatPongMessage