     1→"""
     2→Projects Router
     3→===============
     4→
     5→API endpoints for project management.
     6→Uses project registry for path lookups instead of fixed generations/ directory.
     7→"""
     8→
     9→import re
    10→import shutil
    11→from pathlib import Path
    12→
    13→from fastapi import APIRouter, HTTPException
    14→
    15→from ..schemas import (
    16→    ProjectCreate,
    17→    ProjectDetail,
    18→    ProjectPrompts,
    19→    ProjectPromptsUpdate,
    20→    ProjectStats,
    21→    ProjectSummary,
    22→)
    23→
    24→# Lazy imports to avoid circular dependencies
    25→_imports_initialized = False
    26→_check_spec_exists = None
    27→_scaffold_project_prompts = None
    28→_get_project_prompts_dir = None
    29→_count_passing_tests = None
    30→
    31→
    32→def _init_imports():
    33→    """Lazy import of project-level modules."""
    34→    global _imports_initialized, _check_spec_exists
    35→    global _scaffold_project_prompts, _get_project_prompts_dir
    36→    global _count_passing_tests
    37→
    38→    if _imports_initialized:
    39→        return
    40→
    41→    import sys
    42→    root = Path(__file__).parent.parent.parent
    43→    if str(root) not in sys.path:
    44→        sys.path.insert(0, str(root))
    45→
    46→    from progress import count_passing_tests
    47→    from prompts import get_project_prompts_dir, scaffold_project_prompts
    48→    from start import check_spec_exists
    49→
    50→    _check_spec_exists = check_spec_exists
    51→    _scaffold_project_prompts = scaffold_project_prompts
    52→    _get_project_prompts_dir = get_project_prompts_dir
    53→    _count_passing_tests = count_passing_tests
    54→    _imports_initialized = True
    55→
    56→
    57→def _get_registry_functions():
    58→    """Get registry functions with lazy import."""
    59→    import sys
    60→    root = Path(__file__).parent.parent.parent
    61→    if str(root) not in sys.path:
    62→        sys.path.insert(0, str(root))
    63→
    64→    from registry import (
    65→        get_project_path,
    66→        list_registered_projects,
    67→        register_project,
    68→        unregister_project,
    69→        validate_project_path,
    70→    )
    71→    return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path
    72→
    73→
    74→router = APIRouter(prefix="/api/projects", tags=["projects"])
    75→
    76→
    77→def validate_project_name(name: str) -> str:
    78→    """Validate and sanitize project name to prevent path traversal."""
    79→    if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
    80→        raise HTTPException(
    81→            status_code=400,
    82→            detail="Invalid project name. Use only letters, numbers, hyphens, and underscores (1-50 chars)."
    83→        )
    84→    return name
    85→
    86→
    87→def get_project_stats(project_dir: Path) -> ProjectStats:
    88→    """Get statistics for a project."""
    89→    _init_imports()
    90→    passing, in_progress, total = _count_passing_tests(project_dir)
    91→    percentage = (passing / total * 100) if total > 0 else 0.0
    92→    return ProjectStats(
    93→        passing=passing,
    94→        in_progress=in_progress,
    95→        total=total,
    96→        percentage=round(percentage, 1)
    97→    )
    98→
    99→
   100→@router.get("", response_model=list[ProjectSummary])
   101→async def list_projects():
   102→    """List all registered projects."""
   103→    _init_imports()
   104→    _, _, _, list_registered_projects, validate_project_path = _get_registry_functions()
   105→
   106→    projects = list_registered_projects()
   107→    result = []
   108→
   109→    for name, info in projects.items():
   110→        project_dir = Path(info["path"])
   111→
   112→        # Skip if path no longer exists
   113→        is_valid, _ = validate_project_path(project_dir)
   114→        if not is_valid:
   115→            continue
   116→
   117→        has_spec = _check_spec_exists(project_dir)
   118→        stats = get_project_stats(project_dir)
   119→
   120→        result.append(ProjectSummary(
   121→            name=name,
   122→            path=info["path"],
   123→            has_spec=has_spec,
   124→            stats=stats,
   125→        ))
   126→
   127→    return result
   128→
   129→
   130→@router.post("", response_model=ProjectSummary)
   131→async def create_project(project: ProjectCreate):
   132→    """Create a new project at the specified path."""
   133→    _init_imports()
   134→    register_project, _, get_project_path, _, _ = _get_registry_functions()
   135→
   136→    name = validate_project_name(project.name)
   137→    project_path = Path(project.path).resolve()
   138→
   139→    # Check if project name already registered
   140→    existing = get_project_path(name)
   141→    if existing:
   142→        raise HTTPException(
   143→            status_code=409,
   144→            detail=f"Project '{name}' already exists at {existing}"
   145→        )
   146→
   147→    # Security: Check if path is in a blocked location
   148→    from .filesystem import is_path_blocked
   149→    if is_path_blocked(project_path):
   150→        raise HTTPException(
   151→            status_code=403,
   152→            detail="Cannot create project in system or sensitive directory"
   153→        )
   154→
   155→    # Validate the path is usable
   156→    if project_path.exists():
   157→        if not project_path.is_dir():
   158→            raise HTTPException(
   159→                status_code=400,
   160→                detail="Path exists but is not a directory"
   161→            )
   162→    else:
   163→        # Create the directory
   164→        try:
   165→            project_path.mkdir(parents=True, exist_ok=True)
   166→        except OSError as e:
   167→            raise HTTPException(
   168→                status_code=500,
   169→                detail=f"Failed to create directory: {e}"
   170→            )
   171→
   172→    # Scaffold prompts
   173→    _scaffold_project_prompts(project_path)
   174→
   175→    # Register in registry
   176→    try:
   177→        register_project(name, project_path)
   178→    except Exception as e:
   179→        raise HTTPException(
   180→            status_code=500,
   181→            detail=f"Failed to register project: {e}"
   182→        )
   183→
   184→    return ProjectSummary(
   185→        name=name,
   186→        path=project_path.as_posix(),
   187→        has_spec=False,  # Just created, no spec yet
   188→        stats=ProjectStats(passing=0, total=0, percentage=0.0),
   189→    )
   190→
   191→
   192→@router.get("/{name}", response_model=ProjectDetail)
   193→async def get_project(name: str):
   194→    """Get detailed information about a project."""
   195→    _init_imports()
   196→    _, _, get_project_path, _, _ = _get_registry_functions()
   197→
   198→    name = validate_project_name(name)
   199→    project_dir = get_project_path(name)
   200→
   201→    if not project_dir:
   202→        raise HTTPException(status_code=404, detail=f"Project '{name}' not found in registry")
   203→
   204→    if not project_dir.exists():
   205→        raise HTTPException(status_code=404, detail=f"Project directory no longer exists: {project_dir}")
   206→
   207→    has_spec = _check_spec_exists(project_dir)
   208→    stats = get_project_stats(project_dir)
   209→    prompts_dir = _get_project_prompts_dir(project_dir)
   210→
   211→    return ProjectDetail(
   212→        name=name,
   213→        path=project_dir.as_posix(),
   214→        has_spec=has_spec,
   215→        stats=stats,
   216→        prompts_dir=str(prompts_dir),
   217→    )
   218→
   219→
   220→@router.delete("/{name}")
   221→async def delete_project(name: str, delete_files: bool = False):
   222→    """
   223→    Delete a project from the registry.
   224→
   225→    Args:
   226→        name: Project name to delete
   227→        delete_files: If True, also delete the project directory and files
   228→    """
   229→    _init_imports()
   230→    _, unregister_project, get_project_path, _, _ = _get_registry_functions()
   231→
   232→    name = validate_project_name(name)
   233→    project_dir = get_project_path(name)
   234→
   235→    if not project_dir:
   236→        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
   237→
   238→    # Check if agent is running
   239→    lock_file = project_dir / ".agent.lock"
   240→    if lock_file.exists():
   241→        raise HTTPException(
   242→            status_code=409,
   243→            detail="Cannot delete project while agent is running. Stop the agent first."
   244→        )
   245→
   246→    # Optionally delete files
   247→    if delete_files and project_dir.exists():
   248→        try:
   249→            shutil.rmtree(project_dir)
   250→        except Exception as e:
   251→            raise HTTPException(status_code=500, detail=f"Failed to delete project files: {e}")
   252→
   253→    # Unregister from registry
   254→    unregister_project(name)
   255→
   256→    return {
   257→        "success": True,
   258→        "message": f"Project '{name}' deleted" + (" (files removed)" if delete_files else " (files preserved)")
   259→    }
   260→
   261→
   262→@router.get("/{name}/prompts", response_model=ProjectPrompts)
   263→async def get_project_prompts(name: str):
   264→    """Get the content of project prompt files."""
   265→    _init_imports()
   266→    _, _, get_project_path, _, _ = _get_registry_functions()
   267→
   268→    name = validate_project_name(name)
   269→    project_dir = get_project_path(name)
   270→
   271→    if not project_dir:
   272→        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
   273→
   274→    if not project_dir.exists():
   275→        raise HTTPException(status_code=404, detail="Project directory not found")
   276→
   277→    prompts_dir = _get_project_prompts_dir(project_dir)
   278→
   279→    def read_file(filename: str) -> str:
   280→        filepath = prompts_dir / filename
   281→        if filepath.exists():
   282→            try:
   283→                return filepath.read_text(encoding="utf-8")
   284→            except Exception:
   285→                return ""
   286→        return ""
   287→
   288→    return ProjectPrompts(
   289→        app_spec=read_file("app_spec.txt"),
   290→        initializer_prompt=read_file("initializer_prompt.md"),
   291→        coding_prompt=read_file("coding_prompt.md"),
   292→    )
   293→
   294→
   295→@router.put("/{name}/prompts")
   296→async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
   297→    """Update project prompt files."""
   298→    _init_imports()
   299→    _, _, get_project_path, _, _ = _get_registry_functions()
   300→
   301→    name = validate_project_name(name)
   302→    project_dir = get_project_path(name)
   303→
   304→    if not project_dir:
   305→        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
   306→
   307→    if not project_dir.exists():
   308→        raise HTTPException(status_code=404, detail="Project directory not found")
   309→
   310→    prompts_dir = _get_project_prompts_dir(project_dir)
   311→    prompts_dir.mkdir(parents=True, exist_ok=True)
   312→
   313→    def write_file(filename: str, content: str | None):
   314→        if content is not None:
   315→            filepath = prompts_dir / filename
   316→            filepath.write_text(content, encoding="utf-8")
   317→
   318→    write_file("app_spec.txt", prompts.app_spec)
   319→    write_file("initializer_prompt.md", prompts.initializer_prompt)
   320→    write_file("coding_prompt.md", prompts.coding_prompt)
   321→
   322→    return {"success": True, "message": "Prompts updated"}
   323→
   324→
   325→@router.get("/{name}/stats", response_model=ProjectStats)
   326→async def get_project_stats_endpoint(name: str):
   327→    """Get current progress statistics for a project."""
   328→    _init_imports()
   329→    _, _, get_project_path, _, _ = _get_registry_functions()
   330→
   331→    name = validate_project_name(name)
   332→    project_dir = get_project_path(name)
   333→
   334→    if not project_dir:
   335→        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
   336→
   337→    if not project_dir.exists():
   338→        raise HTTPException(status_code=404, detail="Project directory not found")
   339→
   340→    return get_project_stats(project_dir)
   341→

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

"""
Projects Router
===============

API endpoints for project management.
Uses project registry for path lookups instead of fixed generations/ directory.
"""

import re
import shutil
from pathlib import Path

from fastapi import APIRouter, HTTPException

from ..schemas import (
    ProjectCreate,
    ProjectDetail,
    ProjectPrompts,
    ProjectPromptsUpdate,
    ProjectStats,
    ProjectSummary,
)

# Lazy imports to avoid circular dependencies
_imports_initialized = False
_check_spec_exists = None
_scaffold_project_prompts = None
_get_project_prompts_dir = None
_count_passing_tests = None

def _init_imports():
    """Lazy import of project-level modules."""
    global _imports_initialized, _check_spec_exists
    global _scaffold_project_prompts, _get_project_prompts_dir
    global _count_passing_tests

    if _imports_initialized:
        return

    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from progress import count_passing_tests
    from prompts import get_project_prompts_dir, scaffold_project_prompts
    from start import check_spec_exists

    _check_spec_exists = check_spec_exists
    _scaffold_project_prompts = scaffold_project_prompts
    _get_project_prompts_dir = get_project_prompts_dir
    _count_passing_tests = count_passing_tests
    _imports_initialized = True

def _get_registry_functions():
    """Get registry functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        get_project_path,
        list_registered_projects,
        register_project,
        unregister_project,
        validate_project_path,
    )
    return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path

router = APIRouter(prefix="/api/projects", tags=["projects"])

def validate_project_name(name: str) -> str:
    """Validate and sanitize project name to prevent path traversal."""
    if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
        raise HTTPException(
            status_code=400,
            detail="Invalid project name. Use only letters, numbers, hyphens, and underscores (1-50 chars)."
        )
    return name

def get_project_stats(project_dir: Path) -> ProjectStats:
    """Get statistics for a project."""
    _init_imports()
    passing, in_progress, total = _count_passing_tests(project_dir)
    percentage = (passing / total * 100) if total > 0 else 0.0
    return ProjectStats(
        passing=passing,
        in_progress=in_progress,
        total=total,
        percentage=round(percentage, 1)
    )

@router.get("", response_model=list[ProjectSummary])
async def list_projects():
    """List all registered projects."""
    _init_imports()
    _, _, _, list_registered_projects, validate_project_path = _get_registry_functions()

    projects = list_registered_projects()
    result = []

    for name, info in projects.items():
        project_dir = Path(info["path"])

        # Skip if path no longer exists
        is_valid, _ = validate_project_path(project_dir)
        if not is_valid:
            continue

        has_spec = _check_spec_exists(project_dir)
        stats = get_project_stats(project_dir)

        result.append(ProjectSummary(
            name=name,
            path=info["path"],
            has_spec=has_spec,
            stats=stats,
        ))

    return result

@router.post("", response_model=ProjectSummary)
async def create_project(project: ProjectCreate):
    """Create a new project at the specified path."""
    _init_imports()
    register_project, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(project.name)
    project_path = Path(project.path).resolve()

    # Check if project name already registered
    existing = get_project_path(name)
    if existing:
        raise HTTPException(
            status_code=409,
            detail=f"Project '{name}' already exists at {existing}"
        )

    # Security: Check if path is in a blocked location
    from .filesystem import is_path_blocked
    if is_path_blocked(project_path):
        raise HTTPException(
            status_code=403,
            detail="Cannot create project in system or sensitive directory"
        )

    # Validate the path is usable
    if project_path.exists():
        if not project_path.is_dir():
            raise HTTPException(
                status_code=400,
                detail="Path exists but is not a directory"
            )
    else:
        # Create the directory
        try:
            project_path.mkdir(parents=True, exist_ok=True)
        except OSError as e:
            raise HTTPException(
                status_code=500,
                detail=f"Failed to create directory: {e}"
            )

    # Scaffold prompts
    _scaffold_project_prompts(project_path)

    # Register in registry
    try:
        register_project(name, project_path)
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"Failed to register project: {e}"
        )

    return ProjectSummary(
        name=name,
        path=project_path.as_posix(),
        has_spec=False,  # Just created, no spec yet
        stats=ProjectStats(passing=0, total=0, percentage=0.0),
    )

@router.get("/{name}", response_model=ProjectDetail)
async def get_project(name: str):
    """Get detailed information about a project."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found in registry")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail=f"Project directory no longer exists: {project_dir}")

    has_spec = _check_spec_exists(project_dir)
    stats = get_project_stats(project_dir)
    prompts_dir = _get_project_prompts_dir(project_dir)

    return ProjectDetail(
        name=name,
        path=project_dir.as_posix(),
        has_spec=has_spec,
        stats=stats,
        prompts_dir=str(prompts_dir),
    )

@router.delete("/{name}")
async def delete_project(name: str, delete_files: bool = False):
    """
    Delete a project from the registry.

    Args:
        name: Project name to delete
        delete_files: If True, also delete the project directory and files
    """
    _init_imports()
    _, unregister_project, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    # Check if agent is running
    lock_file = project_dir / ".agent.lock"
    if lock_file.exists():
        raise HTTPException(
            status_code=409,
            detail="Cannot delete project while agent is running. Stop the agent first."
        )

    # Optionally delete files
    if delete_files and project_dir.exists():
        try:
            shutil.rmtree(project_dir)
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"Failed to delete project files: {e}")

    # Unregister from registry
    unregister_project(name)

    return {
        "success": True,
        "message": f"Project '{name}' deleted" + (" (files removed)" if delete_files else " (files preserved)")
    }

@router.get("/{name}/prompts", response_model=ProjectPrompts)
async def get_project_prompts(name: str):
    """Get the content of project prompt files."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    prompts_dir = _get_project_prompts_dir(project_dir)

    def read_file(filename: str) -> str:
        filepath = prompts_dir / filename
        if filepath.exists():
            try:
                return filepath.read_text(encoding="utf-8")
            except Exception:
                return ""
        return ""

    return ProjectPrompts(
        app_spec=read_file("app_spec.txt"),
        initializer_prompt=read_file("initializer_prompt.md"),
        coding_prompt=read_file("coding_prompt.md"),
    )

@router.put("/{name}/prompts")
async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
    """Update project prompt files."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    prompts_dir = _get_project_prompts_dir(project_dir)
    prompts_dir.mkdir(parents=True, exist_ok=True)

    def write_file(filename: str, content: str | None):
        if content is not None:
            filepath = prompts_dir / filename
            filepath.write_text(content, encoding="utf-8")

    write_file("app_spec.txt", prompts.app_spec)
    write_file("initializer_prompt.md", prompts.initializer_prompt)
    write_file("coding_prompt.md", prompts.coding_prompt)

    return {"success": True, "message": "Prompts updated"}

@router.get("/{name}/stats", response_model=ProjectStats)
async def get_project_stats_endpoint(name: str):
    """Get current progress statistics for a project."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    return get_project_stats(project_dir)


     1→"""
     2→Project Registry Module
     3→=======================
     4→
     5→Cross-platform project registry for storing project name to path mappings.
     6→Uses SQLite database stored at ~/.autocoder/registry.db.
     7→"""
     8→
     9→import logging
    10→import os
    11→import re
    12→from contextlib import contextmanager
    13→from datetime import datetime
    14→from pathlib import Path
    15→from typing import Any
    16→
    17→from sqlalchemy import Column, DateTime, String, create_engine
    18→from sqlalchemy.ext.declarative import declarative_base
    19→from sqlalchemy.orm import sessionmaker
    20→
    21→# Module logger
    22→logger = logging.getLogger(__name__)
    23→
    24→
    25→# =============================================================================
    26→# Exceptions
    27→# =============================================================================
    28→
    29→class RegistryError(Exception):
    30→    """Base registry exception."""
    31→    pass
    32→
    33→
    34→class RegistryNotFound(RegistryError):
    35→    """Registry file doesn't exist."""
    36→    pass
    37→
    38→
    39→class RegistryCorrupted(RegistryError):
    40→    """Registry database is corrupted."""
    41→    pass
    42→
    43→
    44→class RegistryPermissionDenied(RegistryError):
    45→    """Can't read/write registry file."""
    46→    pass
    47→
    48→
    49→# =============================================================================
    50→# SQLAlchemy Model
    51→# =============================================================================
    52→
    53→Base = declarative_base()
    54→
    55→
    56→class Project(Base):
    57→    """SQLAlchemy model for registered projects."""
    58→    __tablename__ = "projects"
    59→
    60→    name = Column(String(50), primary_key=True, index=True)
    61→    path = Column(String, nullable=False)  # POSIX format for cross-platform
    62→    created_at = Column(DateTime, nullable=False)
    63→
    64→
    65→# =============================================================================
    66→# Database Connection
    67→# =============================================================================
    68→
    69→# Module-level singleton for database engine
    70→_engine = None
    71→_SessionLocal = None
    72→
    73→
    74→def get_config_dir() -> Path:
    75→    """
    76→    Get the config directory: ~/.autocoder/
    77→
    78→    Returns:
    79→        Path to ~/.autocoder/ (created if it doesn't exist)
    80→    """
    81→    config_dir = Path.home() / ".autocoder"
    82→    config_dir.mkdir(parents=True, exist_ok=True)
    83→    return config_dir
    84→
    85→
    86→def get_registry_path() -> Path:
    87→    """Get the path to the registry database."""
    88→    return get_config_dir() / "registry.db"
    89→
    90→
    91→def _get_engine():
    92→    """
    93→    Get or create the database engine (singleton pattern).
    94→
    95→    Returns:
    96→        Tuple of (engine, SessionLocal)
    97→    """
    98→    global _engine, _SessionLocal
    99→
   100→    if _engine is None:
   101→        db_path = get_registry_path()
   102→        db_url = f"sqlite:///{db_path.as_posix()}"
   103→        _engine = create_engine(db_url, connect_args={"check_same_thread": False})
   104→        Base.metadata.create_all(bind=_engine)
   105→        _SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
   106→        logger.debug("Initialized registry database at: %s", db_path)
   107→
   108→    return _engine, _SessionLocal
   109→
   110→
   111→@contextmanager
   112→def _get_session():
   113→    """
   114→    Context manager for database sessions with automatic commit/rollback.
   115→
   116→    Yields:
   117→        SQLAlchemy session
   118→    """
   119→    _, SessionLocal = _get_engine()
   120→    session = SessionLocal()
   121→    try:
   122→        yield session
   123→        session.commit()
   124→    except Exception:
   125→        session.rollback()
   126→        raise
   127→    finally:
   128→        session.close()
   129→
   130→
   131→# =============================================================================
   132→# Project CRUD Functions
   133→# =============================================================================
   134→
   135→def register_project(name: str, path: Path) -> None:
   136→    """
   137→    Register a new project in the registry.
   138→
   139→    Args:
   140→        name: The project name (unique identifier).
   141→        path: The absolute path to the project directory.
   142→
   143→    Raises:
   144→        ValueError: If project name is invalid or path is not absolute.
   145→        RegistryError: If a project with that name already exists.
   146→    """
   147→    # Validate name
   148→    if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
   149→        raise ValueError(
   150→            "Invalid project name. Use only letters, numbers, hyphens, "
   151→            "and underscores (1-50 chars)."
   152→        )
   153→
   154→    # Ensure path is absolute
   155→    path = Path(path).resolve()
   156→
   157→    with _get_session() as session:
   158→        existing = session.query(Project).filter(Project.name == name).first()
   159→        if existing:
   160→            logger.warning("Attempted to register duplicate project: %s", name)
   161→            raise RegistryError(f"Project '{name}' already exists in registry")
   162→
   163→        project = Project(
   164→            name=name,
   165→            path=path.as_posix(),
   166→            created_at=datetime.now()
   167→        )
   168→        session.add(project)
   169→
   170→    logger.info("Registered project '%s' at path: %s", name, path)
   171→
   172→
   173→def unregister_project(name: str) -> bool:
   174→    """
   175→    Remove a project from the registry.
   176→
   177→    Args:
   178→        name: The project name to remove.
   179→
   180→    Returns:
   181→        True if removed, False if project wasn't found.
   182→    """
   183→    with _get_session() as session:
   184→        project = session.query(Project).filter(Project.name == name).first()
   185→        if not project:
   186→            logger.debug("Attempted to unregister non-existent project: %s", name)
   187→            return False
   188→
   189→        session.delete(project)
   190→
   191→    logger.info("Unregistered project: %s", name)
   192→    return True
   193→
   194→
   195→def get_project_path(name: str) -> Path | None:
   196→    """
   197→    Look up a project's path by name.
   198→
   199→    Args:
   200→        name: The project name.
   201→
   202→    Returns:
   203→        The project Path, or None if not found.
   204→    """
   205→    _, SessionLocal = _get_engine()
   206→    session = SessionLocal()
   207→    try:
   208→        project = session.query(Project).filter(Project.name == name).first()
   209→        if project is None:
   210→            return None
   211→        return Path(project.path)
   212→    finally:
   213→        session.close()
   214→
   215→
   216→def list_registered_projects() -> dict[str, dict[str, Any]]:
   217→    """
   218→    Get all registered projects.
   219→
   220→    Returns:
   221→        Dictionary mapping project names to their info dictionaries.
   222→    """
   223→    _, SessionLocal = _get_engine()
   224→    session = SessionLocal()
   225→    try:
   226→        projects = session.query(Project).all()
   227→        return {
   228→            p.name: {
   229→                "path": p.path,
   230→                "created_at": p.created_at.isoformat() if p.created_at else None
   231→            }
   232→            for p in projects
   233→        }
   234→    finally:
   235→        session.close()
   236→
   237→
   238→def get_project_info(name: str) -> dict[str, Any] | None:
   239→    """
   240→    Get full info about a project.
   241→
   242→    Args:
   243→        name: The project name.
   244→
   245→    Returns:
   246→        Project info dictionary, or None if not found.
   247→    """
   248→    _, SessionLocal = _get_engine()
   249→    session = SessionLocal()
   250→    try:
   251→        project = session.query(Project).filter(Project.name == name).first()
   252→        if project is None:
   253→            return None
   254→        return {
   255→            "path": project.path,
   256→            "created_at": project.created_at.isoformat() if project.created_at else None
   257→        }
   258→    finally:
   259→        session.close()
   260→
   261→
   262→def update_project_path(name: str, new_path: Path) -> bool:
   263→    """
   264→    Update a project's path (for relocating projects).
   265→
   266→    Args:
   267→        name: The project name.
   268→        new_path: The new absolute path.
   269→
   270→    Returns:
   271→        True if updated, False if project wasn't found.
   272→    """
   273→    new_path = Path(new_path).resolve()
   274→
   275→    with _get_session() as session:
   276→        project = session.query(Project).filter(Project.name == name).first()
   277→        if not project:
   278→            return False
   279→
   280→        project.path = new_path.as_posix()
   281→
   282→    return True
   283→
   284→
   285→# =============================================================================
   286→# Validation Functions
   287→# =============================================================================
   288→
   289→def validate_project_path(path: Path) -> tuple[bool, str]:
   290→    """
   291→    Validate that a project path is accessible and writable.
   292→
   293→    Args:
   294→        path: The path to validate.
   295→
   296→    Returns:
   297→        Tuple of (is_valid, error_message).
   298→    """
   299→    path = Path(path).resolve()
   300→
   301→    # Check if path exists
   302→    if not path.exists():
   303→        return False, f"Path does not exist: {path}"
   304→
   305→    # Check if it's a directory
   306→    if not path.is_dir():
   307→        return False, f"Path is not a directory: {path}"
   308→
   309→    # Check read permissions
   310→    if not os.access(path, os.R_OK):
   311→        return False, f"No read permission: {path}"
   312→
   313→    # Check write permissions
   314→    if not os.access(path, os.W_OK):
   315→        return False, f"No write permission: {path}"
   316→
   317→    return True, ""
   318→
   319→
   320→def cleanup_stale_projects() -> list[str]:
   321→    """
   322→    Remove projects from registry whose paths no longer exist.
   323→
   324→    Returns:
   325→        List of removed project names.
   326→    """
   327→    removed = []
   328→
   329→    with _get_session() as session:
   330→        projects = session.query(Project).all()
   331→        for project in projects:
   332→            path = Path(project.path)
   333→            if not path.exists():
   334→                session.delete(project)
   335→                removed.append(project.name)
   336→
   337→    if removed:
   338→        logger.info("Cleaned up stale projects: %s", removed)
   339→
   340→    return removed
   341→
   342→
   343→def list_valid_projects() -> list[dict[str, Any]]:
   344→    """
   345→    List all projects that have valid, accessible paths.
   346→
   347→    Returns:
   348→        List of project info dicts with additional 'name' field.
   349→    """
   350→    _, SessionLocal = _get_engine()
   351→    session = SessionLocal()
   352→    try:
   353→        projects = session.query(Project).all()
   354→        valid = []
   355→        for p in projects:
   356→            path = Path(p.path)
   357→            is_valid, _ = validate_project_path(path)
   358→            if is_valid:
   359→                valid.append({
   360→                    "name": p.name,
   361→                    "path": p.path,
   362→                    "created_at": p.created_at.isoformat() if p.created_at else None
   363→                })
   364→        return valid
   365→    finally:
   366→        session.close()
   367→

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

"""
Project Registry Module
=======================

Cross-platform project registry for storing project name to path mappings.
Uses SQLite database stored at ~/.autocoder/registry.db.
"""

import logging
import os
import re
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from typing import Any

from sqlalchemy import Column, DateTime, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# Module logger
logger = logging.getLogger(__name__)

# =============================================================================
# Exceptions
# =============================================================================

class RegistryError(Exception):
    """Base registry exception."""
    pass

class RegistryNotFound(RegistryError):
    """Registry file doesn't exist."""
    pass

class RegistryCorrupted(RegistryError):
    """Registry database is corrupted."""
    pass

class RegistryPermissionDenied(RegistryError):
    """Can't read/write registry file."""
    pass

# =============================================================================
# SQLAlchemy Model
# =============================================================================

Base = declarative_base()

class Project(Base):
    """SQLAlchemy model for registered projects."""
    __tablename__ = "projects"

    name = Column(String(50), primary_key=True, index=True)
    path = Column(String, nullable=False)  # POSIX format for cross-platform
    created_at = Column(DateTime, nullable=False)

# =============================================================================
# Database Connection
# =============================================================================

# Module-level singleton for database engine
_engine = None
_SessionLocal = None

def get_config_dir() -> Path:
    """
    Get the config directory: ~/.autocoder/

    Returns:
        Path to ~/.autocoder/ (created if it doesn't exist)
    """
    config_dir = Path.home() / ".autocoder"
    config_dir.mkdir(parents=True, exist_ok=True)
    return config_dir

def get_registry_path() -> Path:
    """Get the path to the registry database."""
    return get_config_dir() / "registry.db"

def _get_engine():
    """
    Get or create the database engine (singleton pattern).

    Returns:
        Tuple of (engine, SessionLocal)
    """
    global _engine, _SessionLocal

    if _engine is None:
        db_path = get_registry_path()
        db_url = f"sqlite:///{db_path.as_posix()}"
        _engine = create_engine(db_url, connect_args={"check_same_thread": False})
        Base.metadata.create_all(bind=_engine)
        _SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
        logger.debug("Initialized registry database at: %s", db_path)

    return _engine, _SessionLocal

@contextmanager
def _get_session():
    """
    Context manager for database sessions with automatic commit/rollback.

    Yields:
        SQLAlchemy session
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

# =============================================================================
# Project CRUD Functions
# =============================================================================

def register_project(name: str, path: Path) -> None:
    """
    Register a new project in the registry.

    Args:
        name: The project name (unique identifier).
        path: The absolute path to the project directory.

    Raises:
        ValueError: If project name is invalid or path is not absolute.
        RegistryError: If a project with that name already exists.
    """
    # Validate name
    if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
        raise ValueError(
            "Invalid project name. Use only letters, numbers, hyphens, "
            "and underscores (1-50 chars)."
        )

    # Ensure path is absolute
    path = Path(path).resolve()

    with _get_session() as session:
        existing = session.query(Project).filter(Project.name == name).first()
        if existing:
            logger.warning("Attempted to register duplicate project: %s", name)
            raise RegistryError(f"Project '{name}' already exists in registry")

        project = Project(
            name=name,
            path=path.as_posix(),
            created_at=datetime.now()
        )
        session.add(project)

    logger.info("Registered project '%s' at path: %s", name, path)

def unregister_project(name: str) -> bool:
    """
    Remove a project from the registry.

    Args:
        name: The project name to remove.

    Returns:
        True if removed, False if project wasn't found.
    """
    with _get_session() as session:
        project = session.query(Project).filter(Project.name == name).first()
        if not project:
            logger.debug("Attempted to unregister non-existent project: %s", name)
            return False

        session.delete(project)

    logger.info("Unregistered project: %s", name)
    return True

def get_project_path(name: str) -> Path | None:
    """
    Look up a project's path by name.

    Args:
        name: The project name.

    Returns:
        The project Path, or None if not found.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        project = session.query(Project).filter(Project.name == name).first()
        if project is None:
            return None
        return Path(project.path)
    finally:
        session.close()

def list_registered_projects() -> dict[str, dict[str, Any]]:
    """
    Get all registered projects.

    Returns:
        Dictionary mapping project names to their info dictionaries.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        projects = session.query(Project).all()
        return {
            p.name: {
                "path": p.path,
                "created_at": p.created_at.isoformat() if p.created_at else None
            }
            for p in projects
        }
    finally:
        session.close()

def get_project_info(name: str) -> dict[str, Any] | None:
    """
    Get full info about a project.

    Args:
        name: The project name.

    Returns:
        Project info dictionary, or None if not found.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        project = session.query(Project).filter(Project.name == name).first()
        if project is None:
            return None
        return {
            "path": project.path,
            "created_at": project.created_at.isoformat() if project.created_at else None
        }
    finally:
        session.close()

def update_project_path(name: str, new_path: Path) -> bool:
    """
    Update a project's path (for relocating projects).

    Args:
        name: The project name.
        new_path: The new absolute path.

    Returns:
        True if updated, False if project wasn't found.
    """
    new_path = Path(new_path).resolve()

    with _get_session() as session:
        project = session.query(Project).filter(Project.name == name).first()
        if not project:
            return False

        project.path = new_path.as_posix()

    return True

# =============================================================================
# Validation Functions
# =============================================================================

def validate_project_path(path: Path) -> tuple[bool, str]:
    """
    Validate that a project path is accessible and writable.

    Args:
        path: The path to validate.

    Returns:
        Tuple of (is_valid, error_message).
    """
    path = Path(path).resolve()

    # Check if path exists
    if not path.exists():
        return False, f"Path does not exist: {path}"

    # Check if it's a directory
    if not path.is_dir():
        return False, f"Path is not a directory: {path}"

    # Check read permissions
    if not os.access(path, os.R_OK):
        return False, f"No read permission: {path}"

    # Check write permissions
    if not os.access(path, os.W_OK):
        return False, f"No write permission: {path}"

    return True, ""

def cleanup_stale_projects() -> list[str]:
    """
    Remove projects from registry whose paths no longer exist.

    Returns:
        List of removed project names.
    """
    removed = []

    with _get_session() as session:
        projects = session.query(Project).all()
        for project in projects:
            path = Path(project.path)
            if not path.exists():
                session.delete(project)
                removed.append(project.name)

    if removed:
        logger.info("Cleaned up stale projects: %s", removed)

    return removed

def list_valid_projects() -> list[dict[str, Any]]:
    """
    List all projects that have valid, accessible paths.

    Returns:
        List of project info dicts with additional 'name' field.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        projects = session.query(Project).all()
        valid = []
        for p in projects:
            path = Path(p.path)
            is_valid, _ = validate_project_path(path)
            if is_valid:
                valid.append({
                    "name": p.name,
                    "path": p.path,
                    "created_at": p.created_at.isoformat() if p.created_at else None
                })
        return valid
    finally:
        session.close()


I can see that:

1. The backend already has a `DELETE /api/projects/{name}` endpoint (line 220-259) that:
   - Validates the project name
   - Checks if the agent is running
   - Optionally deletes files if `delete_files=True`
   - Unregisters from the registry

2. The registry already has `unregister_project()` function

So the backend is already complete! I just need to add the UI component for deleting projects. Let me look at the current UI to see how projects are displayed and where to add the delete functionality.


The backend already has a delete endpoint. I just need to add the UI functionality. Let me check the existing UI components:


1→import { useState } from 'react'
     2→import { ChevronDown, Plus, FolderOpen, Loader2 } from 'lucide-react'
     3→import type { ProjectSummary } from '../lib/types'
     4→import { NewProjectModal } from './NewProjectModal'
     5→
     6→interface ProjectSelectorProps {
     7→  projects: ProjectSummary[]
     8→  selectedProject: string | null
     9→  onSelectProject: (name: string | null) => void
    10→  isLoading: boolean
    11→}
    12→
    13→export function ProjectSelector({
    14→  projects,
    15→  selectedProject,
    16→  onSelectProject,
    17→  isLoading,
    18→}: ProjectSelectorProps) {
    19→  const [isOpen, setIsOpen] = useState(false)
    20→  const [showNewProjectModal, setShowNewProjectModal] = useState(false)
    21→
    22→  const handleProjectCreated = (projectName: string) => {
    23→    onSelectProject(projectName)
    24→    setIsOpen(false)
    25→  }
    26→
    27→  const selectedProjectData = projects.find(p => p.name === selectedProject)
    28→
    29→  return (
    30→

    31→      {/* Dropdown Trigger */}
    32→      <button
    33→        onClick={() => setIsOpen(!isOpen)}
    34→        className="neo-btn bg-white text-[var(--color-neo-text)] min-w-[200px] justify-between"
    35→        disabled={isLoading}
    36→      >
    37→        {isLoading ? (
    38→          <Loader2 size={18} className="animate-spin" />
    39→        ) : selectedProject ? (
    40→          <>
    41→
    42→              <FolderOpen size={18} />
    43→              {selectedProject}
    44→
    45→            {selectedProjectData && selectedProjectData.stats.total > 0 && (
    46→
    47→                {selectedProjectData.stats.percentage}%
    48→
    49→            )}
    50→          </>
    51→        ) : (
    52→
    53→            Select Project
    54→
    55→        )}
    56→        <ChevronDown size={18} className={`transition-transform ${isOpen ? 'rotate-180' : ''}`} />
    57→      </button>
    58→
    59→      {/* Dropdown Menu */}
    60→      {isOpen && (
    61→        <>
    62→          {/* Backdrop */}
    63→
 setIsOpen(false)}
    66→          />
    67→
    68→          {/* Menu */}
    69→

    70→            {projects.length > 0 ? (
    71→

    72→                {projects.map(project => (
    73→                  <button
    74→                    key={project.name}
    75→                    onClick={() => {
    76→                      onSelectProject(project.name)
    77→                      setIsOpen(false)
    78→                    }}
    79→                    className={`w-full neo-dropdown-item flex items-center justify-between ${
    80→                      project.name === selectedProject
    81→                        ? 'bg-[var(--color-neo-pending)]'
    82→                        : ''
    83→                    }`}
    84→                  >
    85→
    86→                      <FolderOpen size={16} />
    87→                      {project.name}
    88→
    89→                    {project.stats.total > 0 && (
    90→
    91→                        {project.stats.passing}/{project.stats.total}
    92→
    93→                    )}
    94→                  </button>
    95→                ))}
    96→

    97→            ) : (
    98→

    99→                No projects yet
   100→

   101→            )}
   102→
   103→            {/* Divider */}
   104→

   105→
   106→            {/* Create New */}
   107→            <button
   108→              onClick={() => {
   109→                setShowNewProjectModal(true)
   110→                setIsOpen(false)
   111→              }}
   112→              className="w-full neo-dropdown-item flex items-center gap-2 font-bold"
   113→            >
   114→              <Plus size={16} />
   115→              New Project
   116→            </button>
   117→

   118→        </>
   119→      )}
   120→
   121→      {/* New Project Modal */}
   122→      <NewProjectModal
   123→        isOpen={showNewProjectModal}
   124→        onClose={() => setShowNewProjectModal(false)}
   125→        onProjectCreated={handleProjectCreated}
   126→      />
   127→

   128→  )
   129→}
   130→

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

import { useState } from 'react'
import { ChevronDown, Plus, FolderOpen, Loader2 } from 'lucide-react'
import type { ProjectSummary } from '../lib/types'
import { NewProjectModal } from './NewProjectModal'

interface ProjectSelectorProps {
  projects: ProjectSummary[]
  selectedProject: string | null
  onSelectProject: (name: string | null) => void
  isLoading: boolean
}

export function ProjectSelector({
  projects,
  selectedProject,
  onSelectProject,
  isLoading,
}: ProjectSelectorProps) {
  const [isOpen, setIsOpen] = useState(false)
  const [showNewProjectModal, setShowNewProjectModal] = useState(false)

  const handleProjectCreated = (projectName: string) => {
    onSelectProject(projectName)
    setIsOpen(false)
  }

  const selectedProjectData = projects.find(p => p.name === selectedProject)

  return (

      {/* Dropdown Trigger */}
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="neo-btn bg-white text-[var(--color-neo-text)] min-w-[200px] justify-between"
        disabled={isLoading}
      >
        {isLoading ? (
          <Loader2 size={18} className="animate-spin" />
        ) : selectedProject ? (
          <>

              <FolderOpen size={18} />
              {selectedProject}

            {selectedProjectData && selectedProjectData.stats.total > 0 && (

                {selectedProjectData.stats.percentage}%

            )}
          </>
        ) : (

            Select Project

        )}
        <ChevronDown size={18} className={`transition-transform ${isOpen ? 'rotate-180' : ''}`} />
      </button>

      {/* Dropdown Menu */}
      {isOpen && (
        <>
          {/* Backdrop */}

 setIsOpen(false)}
          />

          {/* Menu */}

            {projects.length > 0 ? (

                {projects.map(project => (
                  <button
                    key={project.name}
                    onClick={() => {
                      onSelectProject(project.name)
                      setIsOpen(false)
                    }}
                    className={`w-full neo-dropdown-item flex items-center justify-between ${
                      project.name === selectedProject
                        ? 'bg-[var(--color-neo-pending)]'
                        : ''
                    }`}
                  >

                      <FolderOpen size={16} />
                      {project.name}

                    {project.stats.total > 0 && (

                        {project.stats.passing}/{project.stats.total}

                    )}
                  </button>
                ))}

            ) : (

                No projects yet

            )}

            {/* Divider */}

            {/* Create New */}
            <button
              onClick={() => {
                setShowNewProjectModal(true)
                setIsOpen(false)
              }}
              className="w-full neo-dropdown-item flex items-center gap-2 font-bold"
            >
              <Plus size={16} />
              New Project
            </button>

        </>
      )}

      {/* New Project Modal */}
      <NewProjectModal
        isOpen={showNewProjectModal}
        onClose={() => setShowNewProjectModal(false)}
        onProjectCreated={handleProjectCreated}
      />

  )
}


     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→export function useImportProject() {
    52→  const queryClient = useQueryClient()
    53→
    54→  return useMutation({
    55→    mutationFn: ({ name, path }: { name: string; path: string }) =>
    56→      api.importProject(name, path),
    57→    onSuccess: () => {
    58→      queryClient.invalidateQueries({ queryKey: ['projects'] })
    59→    },
    60→  })
    61→}
    62→
    63→// ============================================================================
    64→// Features
    65→// ============================================================================
    66→
    67→export function useFeatures(projectName: string | null) {
    68→  return useQuery({
    69→    queryKey: ['features', projectName],
    70→    queryFn: () => api.listFeatures(projectName!),
    71→    enabled: !!projectName,
    72→    refetchInterval: 5000, // Refetch every 5 seconds for real-time updates
    73→  })
    74→}
    75→
    76→export function useCreateFeature(projectName: string) {
    77→  const queryClient = useQueryClient()
    78→
    79→  return useMutation({
    80→    mutationFn: (feature: FeatureCreate) => api.createFeature(projectName, feature),
    81→    onSuccess: () => {
    82→      queryClient.invalidateQueries({ queryKey: ['features', projectName] })
    83→    },
    84→  })
    85→}
    86→
    87→export function useDeleteFeature(projectName: string) {
    88→  const queryClient = useQueryClient()
    89→
    90→  return useMutation({
    91→    mutationFn: (featureId: number) => api.deleteFeature(projectName, featureId),
    92→    onSuccess: () => {
    93→      queryClient.invalidateQueries({ queryKey: ['features', projectName] })
    94→    },
    95→  })
    96→}
    97→
    98→export function useSkipFeature(projectName: string) {
    99→  const queryClient = useQueryClient()
   100→
   101→  return useMutation({
   102→    mutationFn: (featureId: number) => api.skipFeature(projectName, featureId),
   103→    onSuccess: () => {
   104→      queryClient.invalidateQueries({ queryKey: ['features', projectName] })
   105→    },
   106→  })
   107→}
   108→
   109→// ============================================================================
   110→// Agent
   111→// ============================================================================
   112→
   113→export function useAgentStatus(projectName: string | null) {
   114→  return useQuery({
   115→    queryKey: ['agent-status', projectName],
   116→    queryFn: () => api.getAgentStatus(projectName!),
   117→    enabled: !!projectName,
   118→    refetchInterval: 3000, // Poll every 3 seconds
   119→  })
   120→}
   121→
   122→export function useStartAgent(projectName: string) {
   123→  const queryClient = useQueryClient()
   124→
   125→  return useMutation({
   126→    mutationFn: (yoloMode: boolean = false) => api.startAgent(projectName, yoloMode),
   127→    onSuccess: () => {
   128→      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
   129→    },
   130→  })
   131→}
   132→
   133→export function useStopAgent(projectName: string) {
   134→  const queryClient = useQueryClient()
   135→
   136→  return useMutation({
   137→    mutationFn: () => api.stopAgent(projectName),
   138→    onSuccess: () => {
   139→      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
   140→    },
   141→  })
   142→}
   143→
   144→export function usePauseAgent(projectName: string) {
   145→  const queryClient = useQueryClient()
   146→
   147→  return useMutation({
   148→    mutationFn: () => api.pauseAgent(projectName),
   149→    onSuccess: () => {
   150→      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
   151→    },
   152→  })
   153→}
   154→
   155→export function useResumeAgent(projectName: string) {
   156→  const queryClient = useQueryClient()
   157→
   158→  return useMutation({
   159→    mutationFn: () => api.resumeAgent(projectName),
   160→    onSuccess: () => {
   161→      queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
   162→    },
   163→  })
   164→}
   165→
   166→// ============================================================================
   167→// Setup
   168→// ============================================================================
   169→
   170→export function useSetupStatus() {
   171→  return useQuery({
   172→    queryKey: ['setup-status'],
   173→    queryFn: api.getSetupStatus,
   174→    staleTime: 60000, // Cache for 1 minute
   175→  })
   176→}
   177→
   178→export function useHealthCheck() {
   179→  return useQuery({
   180→    queryKey: ['health'],
   181→    queryFn: api.healthCheck,
   182→    retry: false,
   183→  })
   184→}
   185→
   186→// ============================================================================
   187→// Filesystem
   188→// ============================================================================
   189→
   190→export function useListDirectory(path?: string) {
   191→  return useQuery({
   192→    queryKey: ['filesystem', 'list', path],
   193→    queryFn: () => api.listDirectory(path),
   194→  })
   195→}
   196→
   197→export function useCreateDirectory() {
   198→  const queryClient = useQueryClient()
   199→
   200→  return useMutation({
   201→    mutationFn: (path: string) => api.createDirectory(path),
   202→    onSuccess: (_, path) => {
   203→      // Invalidate parent directory listing
   204→      const parentPath = path.split('/').slice(0, -1).join('/') || undefined
   205→      queryClient.invalidateQueries({ queryKey: ['filesystem', 'list', parentPath] })
   206→    },
   207→  })
   208→}
   209→
   210→export function useValidatePath() {
   211→  return useMutation({
   212→    mutationFn: (path: string) => api.validatePath(path),
   213→  })
   214→}
   215→

<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'] })
    },
  })
}

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

  return useMutation({
    mutationFn: ({ name, path }: { name: string; path: string }) =>
      api.importProject(name, path),
    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),
  })
}


I see that `useDeleteProject` already exists, but it just calls `api.deleteProject(name)` without the `delete_files` parameter. Let me check the API file to see what's there:


     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→// Import an existing project (analyzes codebase)
    70→export interface CodebaseAnalysis {
    71→  total_files: number
    72→  file_types: Record<string, number>
    73→  detected_stack: Record<string, string>
    74→  has_claude_md: boolean
    75→  claude_md_content: string | null
    76→  has_readme: boolean
    77→  readme_content: string | null
    78→  directories: string[]
    79→  entry_points: string[]
    80→  project_name: string | null
    81→}
    82→
    83→export interface ProjectImportResponse {
    84→  project: ProjectSummary
    85→  analysis: CodebaseAnalysis
    86→  is_import: boolean
    87→}
    88→
    89→export async function importProject(
    90→  name: string,
    91→  path: string
    92→): Promise<ProjectImportResponse> {
    93→  return fetchJSON('/projects/import', {
    94→    method: 'POST',
    95→    body: JSON.stringify({ name, path }),
    96→  })
    97→}
    98→
    99→export async function getProjectPrompts(name: string): Promise<ProjectPrompts> {
   100→  return fetchJSON(`/projects/${encodeURIComponent(name)}/prompts`)
   101→}
   102→
   103→export async function updateProjectPrompts(
   104→  name: string,
   105→  prompts: Partial<ProjectPrompts>
   106→): Promise<void> {
   107→  await fetchJSON(`/projects/${encodeURIComponent(name)}/prompts`, {
   108→    method: 'PUT',
   109→    body: JSON.stringify(prompts),
   110→  })
   111→}
   112→
   113→// ============================================================================
   114→// Features API
   115→// ============================================================================
   116→
   117→export async function listFeatures(projectName: string): Promise<FeatureListResponse> {
   118→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features`)
   119→}
   120→
   121→export async function createFeature(projectName: string, feature: FeatureCreate): Promise<Feature> {
   122→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features`, {
   123→    method: 'POST',
   124→    body: JSON.stringify(feature),
   125→  })
   126→}
   127→
   128→export async function getFeature(projectName: string, featureId: number): Promise<Feature> {
   129→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}`)
   130→}
   131→
   132→export async function deleteFeature(projectName: string, featureId: number): Promise<void> {
   133→  await fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}`, {
   134→    method: 'DELETE',
   135→  })
   136→}
   137→
   138→export async function skipFeature(projectName: string, featureId: number): Promise<void> {
   139→  await fetchJSON(`/projects/${encodeURIComponent(projectName)}/features/${featureId}/skip`, {
   140→    method: 'PATCH',
   141→  })
   142→}
   143→
   144→// ============================================================================
   145→// Agent API
   146→// ============================================================================
   147→
   148→export async function getAgentStatus(projectName: string): Promise<AgentStatusResponse> {
   149→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/status`)
   150→}
   151→
   152→export async function startAgent(
   153→  projectName: string,
   154→  yoloMode: boolean = false
   155→): Promise<AgentActionResponse> {
   156→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/start`, {
   157→    method: 'POST',
   158→    body: JSON.stringify({ yolo_mode: yoloMode }),
   159→  })
   160→}
   161→
   162→export async function stopAgent(projectName: string): Promise<AgentActionResponse> {
   163→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/stop`, {
   164→    method: 'POST',
   165→  })
   166→}
   167→
   168→export async function pauseAgent(projectName: string): Promise<AgentActionResponse> {
   169→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/pause`, {
   170→    method: 'POST',
   171→  })
   172→}
   173→
   174→export async function resumeAgent(projectName: string): Promise<AgentActionResponse> {
   175→  return fetchJSON(`/projects/${encodeURIComponent(projectName)}/agent/resume`, {
   176→    method: 'POST',
   177→  })
   178→}
   179→
   180→// ============================================================================
   181→// Spec Creation API
   182→// ============================================================================
   183→
   184→export interface SpecFileStatus {
   185→  exists: boolean
   186→  status: 'complete' | 'in_progress' | 'not_started' | 'error' | 'unknown'
   187→  feature_count: number | null
   188→  timestamp: string | null
   189→  files_written: string[]
   190→}
   191→
   192→export async function getSpecStatus(projectName: string): Promise<SpecFileStatus> {
   193→  return fetchJSON(`/spec/status/${encodeURIComponent(projectName)}`)
   194→}
   195→
   196→// ============================================================================
   197→// Setup API
   198→// ============================================================================
   199→
   200→export async function getSetupStatus(): Promise<SetupStatus> {
   201→  return fetchJSON('/setup/status')
   202→}
   203→
   204→export async function healthCheck(): Promise<{ status: string }> {
   205→  return fetchJSON('/health')
   206→}
   207→
   208→// ============================================================================
   209→// Filesystem API
   210→// ============================================================================
   211→
   212→export async function listDirectory(path?: string): Promise<DirectoryListResponse> {
   213→  const params = path ? `?path=${encodeURIComponent(path)}` : ''
   214→  return fetchJSON(`/filesystem/list${params}`)
   215→}
   216→
   217→export async function createDirectory(fullPath: string): Promise<{ success: boolean; path: string }> {
   218→  // Backend expects { parent_path, name }, not { path }
   219→  // Split the full path into parent directory and folder name
   220→
   221→  // Remove trailing slash if present
   222→  const normalizedPath = fullPath.endsWith('/') ? fullPath.slice(0, -1) : fullPath
   223→
   224→  // Find the last path separator
   225→  const lastSlash = normalizedPath.lastIndexOf('/')
   226→
   227→  let parentPath: string
   228→  let name: string
   229→
   230→  // Handle Windows drive root (e.g., "C:/newfolder")
   231→  if (lastSlash === 2 && /^[A-Za-z]:/.test(normalizedPath)) {
   232→    // Path like "C:/newfolder" - parent is "C:/"
   233→    parentPath = normalizedPath.substring(0, 3) // "C:/"
   234→    name = normalizedPath.substring(3)
   235→  } else if (lastSlash > 0) {
   236→    parentPath = normalizedPath.substring(0, lastSlash)
   237→    name = normalizedPath.substring(lastSlash + 1)
   238→  } else if (lastSlash === 0) {
   239→    // Unix root path like "/newfolder"
   240→    parentPath = '/'
   241→    name = normalizedPath.substring(1)
   242→  } else {
   243→    // No slash - invalid path
   244→    throw new Error('Invalid path: must be an absolute path')
   245→  }
   246→
   247→  if (!name) {
   248→    throw new Error('Invalid path: directory name is empty')
   249→  }
   250→
   251→  return fetchJSON('/filesystem/create-directory', {
   252→    method: 'POST',
   253→    body: JSON.stringify({ parent_path: parentPath, name }),
   254→  })
   255→}
   256→
   257→export async function validatePath(path: string): Promise<PathValidationResponse> {
   258→  return fetchJSON('/filesystem/validate', {
   259→    method: 'POST',
   260→    body: JSON.stringify({ path }),
   261→  })
   262→}
   263→
   264→// ============================================================================
   265→// Assistant Chat API
   266→// ============================================================================
   267→
   268→export async function listAssistantConversations(
   269→  projectName: string
   270→): Promise<AssistantConversation[]> {
   271→  return fetchJSON(`/assistant/conversations/${encodeURIComponent(projectName)}`)
   272→}
   273→
   274→export async function getAssistantConversation(
   275→  projectName: string,
   276→  conversationId: number
   277→): Promise<AssistantConversationDetail> {
   278→  return fetchJSON(
   279→    `/assistant/conversations/${encodeURIComponent(projectName)}/${conversationId}`
   280→  )
   281→}
   282→
   283→export async function createAssistantConversation(
   284→  projectName: string
   285→): Promise<AssistantConversation> {
   286→  return fetchJSON(`/assistant/conversations/${encodeURIComponent(projectName)}`, {
   287→    method: 'POST',
   288→  })
   289→}
   290→
   291→export async function deleteAssistantConversation(
   292→  projectName: string,
   293→  conversationId: number
   294→): Promise<void> {
   295→  await fetchJSON(
   296→    `/assistant/conversations/${encodeURIComponent(projectName)}/${conversationId}`,
   297→    { method: 'DELETE' }
   298→  )
   299→}
   300→

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

// Import an existing project (analyzes codebase)
export interface CodebaseAnalysis {
  total_files: number
  file_types: Record<string, number>
  detected_stack: Record<string, string>
  has_claude_md: boolean
  claude_md_content: string | null
  has_readme: boolean
  readme_content: string | null
  directories: string[]
  entry_points: string[]
  project_name: string | null
}

export interface ProjectImportResponse {
  project: ProjectSummary
  analysis: CodebaseAnalysis
  is_import: boolean
}

export async function importProject(
  name: string,
  path: string
): Promise<ProjectImportResponse> {
  return fetchJSON('/projects/import', {
    method: 'POST',
    body: JSON.stringify({ name, path }),
  })
}

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' }
  )
}


Now I understand the full picture. I need to:

1. Update `api.ts` - Add `deleteFiles` parameter to `deleteProject`
2. Update `useProjects.ts` - Update the `useDeleteProject` mutation to accept options
3. Create a `DeleteProjectModal` component with a confirmation dialog and checkbox for deleting files
4. Update `ProjectSelector.tsx` or `App.tsx` to add a delete button/option

Let me implement these changes.


I'll update the API, hook, and create a delete confirmation modal. Let me implement this:


Edit

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

export interface DeleteProjectResponse {
  success: boolean
  message: string
}

export async function deleteProject(
  name: string,
  deleteFiles: boolean = false
): Promise<DeleteProjectResponse> {
  return fetchJSON(`/projects/${encodeURIComponent(name)}?delete_files=${deleteFiles}`, {
    method: 'DELETE',
  })
}


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\ui\src\lib\api.ts has been updated successfully.

export interface DeleteProjectResponse {
  success: boolean
  message: string
}

export async function deleteProject(
  name: string,
  deleteFiles: boolean = false
): Promise<DeleteProjectResponse> {
  return fetchJSON(`/projects/${encodeURIComponent(name)}?delete_files=${deleteFiles}`, {
    method: 'DELETE',
  })
}

/**
 * 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',
  })
}

// Import an existing project (analyzes codebase)
export interface CodebaseAnalysis {
  total_files: number
  file_types: Record<string, number>
  detected_stack: Record<string, string>
  has_claude_md: boolean
  claude_md_content: string | null
  has_readme: boolean
  readme_content: string | null
  directories: string[]
  entry_points: string[]
  project_name: string | null
}

export interface ProjectImportResponse {
  project: ProjectSummary
  analysis: CodebaseAnalysis
  is_import: boolean
}

export async function importProject(
  name: string,
  path: string
): Promise<ProjectImportResponse> {
  return fetchJSON('/projects/import', {
    method: 'POST',
    body: JSON.stringify({ name, path }),
  })
}

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' }
  )
}

-export async function deleteProject(name: string): Promise<void> {

-  await fetchJSON(`/projects/${encodeURIComponent(name)}`, {

+export interface DeleteProjectResponse {

+  return fetchJSON(`/projects/${encodeURIComponent(name)}?delete_files=${deleteFiles}`, {


Now update the useProjects hook to accept the deleteFiles option.