     1→"""
     2→Filesystem Router
     3→==================
     4→
     5→API endpoints for browsing the filesystem for project folder selection.
     6→Provides cross-platform support for Windows, macOS, and Linux.
     7→"""
     8→
     9→import logging
    10→import os
    11→import re
    12→import sys
    13→from pathlib import Path
    14→
    15→from fastapi import APIRouter, HTTPException, Query
    16→
    17→# Module logger
    18→logger = logging.getLogger(__name__)
    19→
    20→from ..schemas import (
    21→    CreateDirectoryRequest,
    22→    DirectoryEntry,
    23→    DirectoryListResponse,
    24→    DriveInfo,
    25→    PathValidationResponse,
    26→)
    27→
    28→router = APIRouter(prefix="/api/filesystem", tags=["filesystem"])
    29→
    30→
    31→# =============================================================================
    32→# Platform-Specific Blocked Paths
    33→# =============================================================================
    34→
    35→# Windows blocked paths
    36→WINDOWS_BLOCKED = {
    37→    "C:\\Windows",
    38→    "C:\\Program Files",
    39→    "C:\\Program Files (x86)",
    40→    "C:\\ProgramData",
    41→    "C:\\System Volume Information",
    42→    "C:\\$Recycle.Bin",
    43→    "C:\\Recovery",
    44→}
    45→
    46→# macOS blocked paths
    47→MACOS_BLOCKED = {
    48→    "/System",
    49→    "/Library",
    50→    "/private",
    51→    "/usr",
    52→    "/bin",
    53→    "/sbin",
    54→    "/etc",
    55→    "/var",
    56→    "/Volumes",
    57→    "/cores",
    58→    "/opt",
    59→}
    60→
    61→# Linux blocked paths
    62→LINUX_BLOCKED = {
    63→    "/etc",
    64→    "/var",
    65→    "/usr",
    66→    "/bin",
    67→    "/sbin",
    68→    "/boot",
    69→    "/proc",
    70→    "/sys",
    71→    "/dev",
    72→    "/root",
    73→    "/lib",
    74→    "/lib64",
    75→    "/run",
    76→    "/tmp",
    77→    "/opt",
    78→}
    79→
    80→# Universal blocked paths (relative to home directory)
    81→UNIVERSAL_BLOCKED_RELATIVE = {
    82→    ".ssh",
    83→    ".aws",
    84→    ".gnupg",
    85→    ".config/gh",
    86→    ".netrc",
    87→    ".docker",
    88→    ".kube",
    89→    ".terraform",
    90→}
    91→
    92→# Patterns for files that should not be shown
    93→HIDDEN_PATTERNS = [
    94→    r"^\.env",           # .env files
    95→    r".*\.key$",         # Key files
    96→    r".*\.pem$",         # PEM files
    97→    r".*credentials.*",  # Credential files
    98→    r".*secrets.*",      # Secrets files
    99→]
   100→
   101→
   102→def get_blocked_paths() -> set[Path]:
   103→    """Get the set of blocked paths for the current platform."""
   104→    home = Path.home()
   105→    blocked = set()
   106→
   107→    # Add platform-specific blocked paths
   108→    if sys.platform == "win32":
   109→        for p in WINDOWS_BLOCKED:
   110→            blocked.add(Path(p).resolve())
   111→    elif sys.platform == "darwin":
   112→        for p in MACOS_BLOCKED:
   113→            blocked.add(Path(p).resolve())
   114→    else:  # Linux
   115→        for p in LINUX_BLOCKED:
   116→            blocked.add(Path(p).resolve())
   117→
   118→    # Add universal blocked paths (relative to home)
   119→    for rel in UNIVERSAL_BLOCKED_RELATIVE:
   120→        blocked.add((home / rel).resolve())
   121→
   122→    return blocked
   123→
   124→
   125→def is_path_blocked(path: Path) -> bool:
   126→    """Check if a path is in the blocked list."""
   127→    try:
   128→        resolved = path.resolve()
   129→    except (OSError, ValueError):
   130→        return True  # Can't resolve = blocked
   131→
   132→    blocked_paths = get_blocked_paths()
   133→
   134→    # Check if path is exactly a blocked path or inside one
   135→    for blocked in blocked_paths:
   136→        try:
   137→            resolved.relative_to(blocked)
   138→            return True
   139→        except ValueError:
   140→            pass
   141→
   142→        # Also check if blocked is inside path (for parent directories)
   143→        if resolved == blocked:
   144→            return True
   145→
   146→    return False
   147→
   148→
   149→def is_hidden_file(path: Path) -> bool:
   150→    """Check if a file/directory is hidden (cross-platform)."""
   151→    name = path.name
   152→
   153→    # Unix-style: starts with dot
   154→    if name.startswith('.'):
   155→        return True
   156→
   157→    # Windows: check FILE_ATTRIBUTE_HIDDEN
   158→    if sys.platform == "win32":
   159→        try:
   160→            import ctypes
   161→            attrs = ctypes.windll.kernel32.GetFileAttributesW(str(path))
   162→            if attrs != -1 and (attrs & 0x02):  # FILE_ATTRIBUTE_HIDDEN
   163→                return True
   164→        except Exception:
   165→            pass
   166→
   167→    return False
   168→
   169→
   170→def matches_blocked_pattern(name: str) -> bool:
   171→    """Check if filename matches a blocked pattern."""
   172→    for pattern in HIDDEN_PATTERNS:
   173→        if re.match(pattern, name, re.IGNORECASE):
   174→            return True
   175→    return False
   176→
   177→
   178→def is_unc_path(path_str: str) -> bool:
   179→    """Check if path is a Windows UNC path (network share)."""
   180→    return path_str.startswith("\\\\") or path_str.startswith("//")
   181→
   182→
   183→# =============================================================================
   184→# Endpoints
   185→# =============================================================================
   186→
   187→@router.get("/list", response_model=DirectoryListResponse)
   188→async def list_directory(
   189→    path: str | None = Query(None, description="Directory path to list (defaults to home)"),
   190→    show_hidden: bool = Query(False, description="Include hidden files"),
   191→):
   192→    """
   193→    List contents of a directory.
   194→
   195→    Returns directories only (for folder selection).
   196→    On Windows, includes available drives.
   197→    """
   198→    # Default to home directory
   199→    if path is None or path == "":
   200→        target = Path.home()
   201→    else:
   202→        # Security: Block UNC paths
   203→        if is_unc_path(path):
   204→            logger.warning("Blocked UNC path access attempt: %s", path)
   205→            raise HTTPException(
   206→                status_code=403,
   207→                detail="Network paths (UNC) are not allowed"
   208→            )
   209→        target = Path(path)
   210→
   211→    # Resolve symlinks and get absolute path
   212→    try:
   213→        target = target.resolve()
   214→    except (OSError, ValueError) as e:
   215→        raise HTTPException(status_code=400, detail=f"Invalid path: {e}")
   216→
   217→    # Security: Check if path is blocked
   218→    if is_path_blocked(target):
   219→        logger.warning("Blocked access to restricted path: %s", target)
   220→        raise HTTPException(
   221→            status_code=403,
   222→            detail="Access to this directory is not allowed"
   223→        )
   224→
   225→    # Check if path exists and is a directory
   226→    if not target.exists():
   227→        raise HTTPException(status_code=404, detail="Directory not found")
   228→
   229→    if not target.is_dir():
   230→        raise HTTPException(status_code=400, detail="Path is not a directory")
   231→
   232→    # Check read permission
   233→    if not os.access(target, os.R_OK):
   234→        raise HTTPException(status_code=403, detail="No read permission")
   235→
   236→    # List directory contents
   237→    entries = []
   238→    try:
   239→        for item in sorted(target.iterdir(), key=lambda x: x.name.lower()):
   240→            # Skip if blocked pattern
   241→            if matches_blocked_pattern(item.name):
   242→                continue
   243→
   244→            # Check if hidden
   245→            hidden = is_hidden_file(item)
   246→            if hidden and not show_hidden:
   247→                continue
   248→
   249→            # Security: Skip if item path is blocked
   250→            if is_path_blocked(item):
   251→                continue
   252→
   253→            # Only include directories for folder browsing
   254→            if item.is_dir():
   255→                try:
   256→                    # Check if directory has any subdirectories
   257→                    has_children = False
   258→                    try:
   259→                        for child in item.iterdir():
   260→                            if child.is_dir() and not is_path_blocked(child):
   261→                                has_children = True
   262→                                break
   263→                    except (PermissionError, OSError):
   264→                        pass  # Can't read = assume no children
   265→
   266→                    entries.append(DirectoryEntry(
   267→                        name=item.name,
   268→                        path=item.as_posix(),
   269→                        is_directory=True,
   270→                        is_hidden=hidden,
   271→                        size=None,
   272→                        has_children=has_children,
   273→                    ))
   274→                except Exception:
   275→                    pass  # Skip items we can't process
   276→
   277→    except PermissionError:
   278→        raise HTTPException(status_code=403, detail="Permission denied")
   279→    except OSError as e:
   280→        raise HTTPException(status_code=500, detail=f"Error reading directory: {e}")
   281→
   282→    # Calculate parent path
   283→    parent_path = None
   284→    if target != target.parent:  # Not at root
   285→        parent = target.parent
   286→        # Don't expose parent if it's blocked
   287→        if not is_path_blocked(parent):
   288→            parent_path = parent.as_posix()
   289→
   290→    # Get drives on Windows
   291→    drives = None
   292→    if sys.platform == "win32":
   293→        drives = get_windows_drives()
   294→
   295→    return DirectoryListResponse(
   296→        current_path=target.as_posix(),
   297→        parent_path=parent_path,
   298→        entries=entries,
   299→        drives=drives,
   300→    )
   301→
   302→
   303→@router.get("/drives", response_model=list[DriveInfo] | None)
   304→async def list_drives():
   305→    """
   306→    List available drives (Windows only).
   307→
   308→    Returns null on non-Windows platforms.
   309→    """
   310→    if sys.platform != "win32":
   311→        return None
   312→
   313→    return get_windows_drives()
   314→
   315→
   316→def get_windows_drives() -> list[DriveInfo]:
   317→    """Get list of available drives on Windows."""
   318→    drives = []
   319→
   320→    try:
   321→        import ctypes
   322→        import string
   323→
   324→        # Get bitmask of available drives
   325→        bitmask = ctypes.windll.kernel32.GetLogicalDrives()
   326→
   327→        for i, letter in enumerate(string.ascii_uppercase):
   328→            if bitmask & (1 << i):
   329→                drive_path = f"{letter}:\\"
   330→                try:
   331→                    # Try to get volume label
   332→                    volume_name = ctypes.create_unicode_buffer(1024)
   333→                    ctypes.windll.kernel32.GetVolumeInformationW(
   334→                        drive_path,
   335→                        volume_name,
   336→                        1024,
   337→                        None, None, None, None, 0
   338→                    )
   339→                    label = volume_name.value or f"Local Disk ({letter}:)"
   340→                except Exception:
   341→                    label = f"Drive ({letter}:)"
   342→
   343→                # Check if drive is accessible
   344→                available = os.path.exists(drive_path)
   345→
   346→                drives.append(DriveInfo(
   347→                    letter=letter,
   348→                    label=label,
   349→                    available=available,
   350→                ))
   351→    except Exception:
   352→        # Fallback: just list C: drive
   353→        drives.append(DriveInfo(letter="C", label="Local Disk (C:)", available=True))
   354→
   355→    return drives
   356→
   357→
   358→@router.post("/validate", response_model=PathValidationResponse)
   359→async def validate_path(path: str = Query(..., description="Path to validate")):
   360→    """
   361→    Validate if a path is accessible and writable.
   362→
   363→    Used to check a path before creating a project there.
   364→    """
   365→    # Security: Block UNC paths
   366→    if is_unc_path(path):
   367→        return PathValidationResponse(
   368→            valid=False,
   369→            exists=False,
   370→            is_directory=False,
   371→            can_read=False,
   372→            can_write=False,
   373→            message="Network paths (UNC) are not allowed",
   374→        )
   375→
   376→    try:
   377→        target = Path(path).resolve()
   378→    except (OSError, ValueError) as e:
   379→        return PathValidationResponse(
   380→            valid=False,
   381→            exists=False,
   382→            is_directory=False,
   383→            can_read=False,
   384→            can_write=False,
   385→            message=f"Invalid path: {e}",
   386→        )
   387→
   388→    # Security: Check if blocked
   389→    if is_path_blocked(target):
   390→        return PathValidationResponse(
   391→            valid=False,
   392→            exists=target.exists(),
   393→            is_directory=target.is_dir() if target.exists() else False,
   394→            can_read=False,
   395→            can_write=False,
   396→            message="Access to this directory is not allowed",
   397→        )
   398→
   399→    exists = target.exists()
   400→    is_dir = target.is_dir() if exists else False
   401→    can_read = os.access(target, os.R_OK) if exists else False
   402→    can_write = os.access(target, os.W_OK) if exists else False
   403→
   404→    # For non-existent paths, check if parent is writable
   405→    if not exists:
   406→        parent = target.parent
   407→        parent_exists = parent.exists()
   408→        parent_writable = os.access(parent, os.W_OK) if parent_exists else False
   409→        can_write = parent_writable
   410→
   411→    valid = is_dir and can_read and can_write if exists else can_write
   412→    message = ""
   413→    if not exists:
   414→        message = "Directory does not exist (will be created)"
   415→    elif not is_dir:
   416→        message = "Path is not a directory"
   417→    elif not can_read:
   418→        message = "No read permission"
   419→    elif not can_write:
   420→        message = "No write permission"
   421→
   422→    return PathValidationResponse(
   423→        valid=valid,
   424→        exists=exists,
   425→        is_directory=is_dir,
   426→        can_read=can_read,
   427→        can_write=can_write,
   428→        message=message,
   429→    )
   430→
   431→
   432→@router.post("/create-directory")
   433→async def create_directory(request: CreateDirectoryRequest):
   434→    """
   435→    Create a new directory inside a parent directory.
   436→
   437→    Used for creating project folders from the folder browser.
   438→    """
   439→    # Validate directory name
   440→    name = request.name.strip()
   441→    if not name:
   442→        raise HTTPException(status_code=400, detail="Directory name cannot be empty")
   443→
   444→    # Security: Block special directory names that could enable traversal
   445→    if name in ('.', '..') or '..' in name:
   446→        raise HTTPException(
   447→            status_code=400,
   448→            detail="Invalid directory name"
   449→        )
   450→
   451→    # Security: Check for invalid characters
   452→    invalid_chars = '<>:"/\\|?*' if sys.platform == "win32" else '/'
   453→    if any(c in name for c in invalid_chars):
   454→        raise HTTPException(
   455→            status_code=400,
   456→            detail="Directory name contains invalid characters"
   457→        )
   458→
   459→    # Security: Block UNC paths
   460→    if is_unc_path(request.parent_path):
   461→        raise HTTPException(status_code=403, detail="Network paths are not allowed")
   462→
   463→    try:
   464→        parent = Path(request.parent_path).resolve()
   465→    except (OSError, ValueError) as e:
   466→        raise HTTPException(status_code=400, detail=f"Invalid parent path: {e}")
   467→
   468→    # Security: Check if parent is blocked
   469→    if is_path_blocked(parent):
   470→        raise HTTPException(
   471→            status_code=403,
   472→            detail="Cannot create directory in this location"
   473→        )
   474→
   475→    # Check parent exists and is writable
   476→    if not parent.exists():
   477→        raise HTTPException(status_code=404, detail="Parent directory not found")
   478→
   479→    if not parent.is_dir():
   480→        raise HTTPException(status_code=400, detail="Parent path is not a directory")
   481→
   482→    if not os.access(parent, os.W_OK):
   483→        raise HTTPException(status_code=403, detail="No write permission")
   484→
   485→    # Create the new directory
   486→    new_dir = parent / name
   487→
   488→    if new_dir.exists():
   489→        raise HTTPException(status_code=409, detail="Directory already exists")
   490→
   491→    try:
   492→        new_dir.mkdir(parents=False, exist_ok=False)
   493→        logger.info("Created directory: %s", new_dir)
   494→    except OSError as e:
   495→        logger.error("Failed to create directory %s: %s", new_dir, e)
   496→        raise HTTPException(status_code=500, detail=f"Failed to create directory: {e}")
   497→
   498→    return {
   499→        "success": True,
   500→        "path": new_dir.as_posix(),
   501→        "message": f"Created directory: {name}",
   502→    }
   503→
   504→
   505→@router.get("/home")
   506→async def get_home_directory():
   507→    """Get the user's home directory path."""
   508→    home = Path.home()
   509→    return {
   510→        "path": home.as_posix(),
   511→        "display_path": str(home),
   512→    }
   513→

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

"""
Filesystem Router
==================

API endpoints for browsing the filesystem for project folder selection.
Provides cross-platform support for Windows, macOS, and Linux.
"""

import logging
import os
import re
import sys
from pathlib import Path

from fastapi import APIRouter, HTTPException, Query

# Module logger
logger = logging.getLogger(__name__)

from ..schemas import (
    CreateDirectoryRequest,
    DirectoryEntry,
    DirectoryListResponse,
    DriveInfo,
    PathValidationResponse,
)

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

# =============================================================================
# Platform-Specific Blocked Paths
# =============================================================================

# Windows blocked paths
WINDOWS_BLOCKED = {
    "C:\\Windows",
    "C:\\Program Files",
    "C:\\Program Files (x86)",
    "C:\\ProgramData",
    "C:\\System Volume Information",
    "C:\\$Recycle.Bin",
    "C:\\Recovery",
}

# macOS blocked paths
MACOS_BLOCKED = {
    "/System",
    "/Library",
    "/private",
    "/usr",
    "/bin",
    "/sbin",
    "/etc",
    "/var",
    "/Volumes",
    "/cores",
    "/opt",
}

# Linux blocked paths
LINUX_BLOCKED = {
    "/etc",
    "/var",
    "/usr",
    "/bin",
    "/sbin",
    "/boot",
    "/proc",
    "/sys",
    "/dev",
    "/root",
    "/lib",
    "/lib64",
    "/run",
    "/tmp",
    "/opt",
}

# Universal blocked paths (relative to home directory)
UNIVERSAL_BLOCKED_RELATIVE = {
    ".ssh",
    ".aws",
    ".gnupg",
    ".config/gh",
    ".netrc",
    ".docker",
    ".kube",
    ".terraform",
}

# Patterns for files that should not be shown
HIDDEN_PATTERNS = [
    r"^\.env",           # .env files
    r".*\.key$",         # Key files
    r".*\.pem$",         # PEM files
    r".*credentials.*",  # Credential files
    r".*secrets.*",      # Secrets files
]

def get_blocked_paths() -> set[Path]:
    """Get the set of blocked paths for the current platform."""
    home = Path.home()
    blocked = set()

    # Add platform-specific blocked paths
    if sys.platform == "win32":
        for p in WINDOWS_BLOCKED:
            blocked.add(Path(p).resolve())
    elif sys.platform == "darwin":
        for p in MACOS_BLOCKED:
            blocked.add(Path(p).resolve())
    else:  # Linux
        for p in LINUX_BLOCKED:
            blocked.add(Path(p).resolve())

    # Add universal blocked paths (relative to home)
    for rel in UNIVERSAL_BLOCKED_RELATIVE:
        blocked.add((home / rel).resolve())

    return blocked

def is_path_blocked(path: Path) -> bool:
    """Check if a path is in the blocked list."""
    try:
        resolved = path.resolve()
    except (OSError, ValueError):
        return True  # Can't resolve = blocked

    blocked_paths = get_blocked_paths()

    # Check if path is exactly a blocked path or inside one
    for blocked in blocked_paths:
        try:
            resolved.relative_to(blocked)
            return True
        except ValueError:
            pass

        # Also check if blocked is inside path (for parent directories)
        if resolved == blocked:
            return True

    return False

def is_hidden_file(path: Path) -> bool:
    """Check if a file/directory is hidden (cross-platform)."""
    name = path.name

    # Unix-style: starts with dot
    if name.startswith('.'):
        return True

    # Windows: check FILE_ATTRIBUTE_HIDDEN
    if sys.platform == "win32":
        try:
            import ctypes
            attrs = ctypes.windll.kernel32.GetFileAttributesW(str(path))
            if attrs != -1 and (attrs & 0x02):  # FILE_ATTRIBUTE_HIDDEN
                return True
        except Exception:
            pass

    return False

def matches_blocked_pattern(name: str) -> bool:
    """Check if filename matches a blocked pattern."""
    for pattern in HIDDEN_PATTERNS:
        if re.match(pattern, name, re.IGNORECASE):
            return True
    return False

def is_unc_path(path_str: str) -> bool:
    """Check if path is a Windows UNC path (network share)."""
    return path_str.startswith("\\\\") or path_str.startswith("//")

# =============================================================================
# Endpoints
# =============================================================================

@router.get("/list", response_model=DirectoryListResponse)
async def list_directory(
    path: str | None = Query(None, description="Directory path to list (defaults to home)"),
    show_hidden: bool = Query(False, description="Include hidden files"),
):
    """
    List contents of a directory.

    Returns directories only (for folder selection).
    On Windows, includes available drives.
    """
    # Default to home directory
    if path is None or path == "":
        target = Path.home()
    else:
        # Security: Block UNC paths
        if is_unc_path(path):
            logger.warning("Blocked UNC path access attempt: %s", path)
            raise HTTPException(
                status_code=403,
                detail="Network paths (UNC) are not allowed"
            )
        target = Path(path)

    # Resolve symlinks and get absolute path
    try:
        target = target.resolve()
    except (OSError, ValueError) as e:
        raise HTTPException(status_code=400, detail=f"Invalid path: {e}")

    # Security: Check if path is blocked
    if is_path_blocked(target):
        logger.warning("Blocked access to restricted path: %s", target)
        raise HTTPException(
            status_code=403,
            detail="Access to this directory is not allowed"
        )

    # Check if path exists and is a directory
    if not target.exists():
        raise HTTPException(status_code=404, detail="Directory not found")

    if not target.is_dir():
        raise HTTPException(status_code=400, detail="Path is not a directory")

    # Check read permission
    if not os.access(target, os.R_OK):
        raise HTTPException(status_code=403, detail="No read permission")

    # List directory contents
    entries = []
    try:
        for item in sorted(target.iterdir(), key=lambda x: x.name.lower()):
            # Skip if blocked pattern
            if matches_blocked_pattern(item.name):
                continue

            # Check if hidden
            hidden = is_hidden_file(item)
            if hidden and not show_hidden:
                continue

            # Security: Skip if item path is blocked
            if is_path_blocked(item):
                continue

            # Only include directories for folder browsing
            if item.is_dir():
                try:
                    # Check if directory has any subdirectories
                    has_children = False
                    try:
                        for child in item.iterdir():
                            if child.is_dir() and not is_path_blocked(child):
                                has_children = True
                                break
                    except (PermissionError, OSError):
                        pass  # Can't read = assume no children

                    entries.append(DirectoryEntry(
                        name=item.name,
                        path=item.as_posix(),
                        is_directory=True,
                        is_hidden=hidden,
                        size=None,
                        has_children=has_children,
                    ))
                except Exception:
                    pass  # Skip items we can't process

    except PermissionError:
        raise HTTPException(status_code=403, detail="Permission denied")
    except OSError as e:
        raise HTTPException(status_code=500, detail=f"Error reading directory: {e}")

    # Calculate parent path
    parent_path = None
    if target != target.parent:  # Not at root
        parent = target.parent
        # Don't expose parent if it's blocked
        if not is_path_blocked(parent):
            parent_path = parent.as_posix()

    # Get drives on Windows
    drives = None
    if sys.platform == "win32":
        drives = get_windows_drives()

    return DirectoryListResponse(
        current_path=target.as_posix(),
        parent_path=parent_path,
        entries=entries,
        drives=drives,
    )

@router.get("/drives", response_model=list[DriveInfo] | None)
async def list_drives():
    """
    List available drives (Windows only).

    Returns null on non-Windows platforms.
    """
    if sys.platform != "win32":
        return None

    return get_windows_drives()

def get_windows_drives() -> list[DriveInfo]:
    """Get list of available drives on Windows."""
    drives = []

    try:
        import ctypes
        import string

        # Get bitmask of available drives
        bitmask = ctypes.windll.kernel32.GetLogicalDrives()

        for i, letter in enumerate(string.ascii_uppercase):
            if bitmask & (1 << i):
                drive_path = f"{letter}:\\"
                try:
                    # Try to get volume label
                    volume_name = ctypes.create_unicode_buffer(1024)
                    ctypes.windll.kernel32.GetVolumeInformationW(
                        drive_path,
                        volume_name,
                        1024,
                        None, None, None, None, 0
                    )
                    label = volume_name.value or f"Local Disk ({letter}:)"
                except Exception:
                    label = f"Drive ({letter}:)"

                # Check if drive is accessible
                available = os.path.exists(drive_path)

                drives.append(DriveInfo(
                    letter=letter,
                    label=label,
                    available=available,
                ))
    except Exception:
        # Fallback: just list C: drive
        drives.append(DriveInfo(letter="C", label="Local Disk (C:)", available=True))

    return drives

@router.post("/validate", response_model=PathValidationResponse)
async def validate_path(path: str = Query(..., description="Path to validate")):
    """
    Validate if a path is accessible and writable.

    Used to check a path before creating a project there.
    """
    # Security: Block UNC paths
    if is_unc_path(path):
        return PathValidationResponse(
            valid=False,
            exists=False,
            is_directory=False,
            can_read=False,
            can_write=False,
            message="Network paths (UNC) are not allowed",
        )

    try:
        target = Path(path).resolve()
    except (OSError, ValueError) as e:
        return PathValidationResponse(
            valid=False,
            exists=False,
            is_directory=False,
            can_read=False,
            can_write=False,
            message=f"Invalid path: {e}",
        )

    # Security: Check if blocked
    if is_path_blocked(target):
        return PathValidationResponse(
            valid=False,
            exists=target.exists(),
            is_directory=target.is_dir() if target.exists() else False,
            can_read=False,
            can_write=False,
            message="Access to this directory is not allowed",
        )

    exists = target.exists()
    is_dir = target.is_dir() if exists else False
    can_read = os.access(target, os.R_OK) if exists else False
    can_write = os.access(target, os.W_OK) if exists else False

    # For non-existent paths, check if parent is writable
    if not exists:
        parent = target.parent
        parent_exists = parent.exists()
        parent_writable = os.access(parent, os.W_OK) if parent_exists else False
        can_write = parent_writable

    valid = is_dir and can_read and can_write if exists else can_write
    message = ""
    if not exists:
        message = "Directory does not exist (will be created)"
    elif not is_dir:
        message = "Path is not a directory"
    elif not can_read:
        message = "No read permission"
    elif not can_write:
        message = "No write permission"

    return PathValidationResponse(
        valid=valid,
        exists=exists,
        is_directory=is_dir,
        can_read=can_read,
        can_write=can_write,
        message=message,
    )

@router.post("/create-directory")
async def create_directory(request: CreateDirectoryRequest):
    """
    Create a new directory inside a parent directory.

    Used for creating project folders from the folder browser.
    """
    # Validate directory name
    name = request.name.strip()
    if not name:
        raise HTTPException(status_code=400, detail="Directory name cannot be empty")

    # Security: Block special directory names that could enable traversal
    if name in ('.', '..') or '..' in name:
        raise HTTPException(
            status_code=400,
            detail="Invalid directory name"
        )

    # Security: Check for invalid characters
    invalid_chars = '<>:"/\\|?*' if sys.platform == "win32" else '/'
    if any(c in name for c in invalid_chars):
        raise HTTPException(
            status_code=400,
            detail="Directory name contains invalid characters"
        )

    # Security: Block UNC paths
    if is_unc_path(request.parent_path):
        raise HTTPException(status_code=403, detail="Network paths are not allowed")

    try:
        parent = Path(request.parent_path).resolve()
    except (OSError, ValueError) as e:
        raise HTTPException(status_code=400, detail=f"Invalid parent path: {e}")

    # Security: Check if parent is blocked
    if is_path_blocked(parent):
        raise HTTPException(
            status_code=403,
            detail="Cannot create directory in this location"
        )

    # Check parent exists and is writable
    if not parent.exists():
        raise HTTPException(status_code=404, detail="Parent directory not found")

    if not parent.is_dir():
        raise HTTPException(status_code=400, detail="Parent path is not a directory")

    if not os.access(parent, os.W_OK):
        raise HTTPException(status_code=403, detail="No write permission")

    # Create the new directory
    new_dir = parent / name

    if new_dir.exists():
        raise HTTPException(status_code=409, detail="Directory already exists")

    try:
        new_dir.mkdir(parents=False, exist_ok=False)
        logger.info("Created directory: %s", new_dir)
    except OSError as e:
        logger.error("Failed to create directory %s: %s", new_dir, e)
        raise HTTPException(status_code=500, detail=f"Failed to create directory: {e}")

    return {
        "success": True,
        "path": new_dir.as_posix(),
        "message": f"Created directory: {name}",
    }

@router.get("/home")
async def get_home_directory():
    """Get the user's home directory path."""
    home = Path.home()
    return {
        "path": home.as_posix(),
        "display_path": str(home),
    }


I need to:
1. Create a filesystem router for the remix backend
2. Create a FolderBrowser component for the remix frontend
3. Update the CreateProjectModal to use the folder browserLet me start by creating the filesystem router for the backend. I'll adapt it from the autocoder-master version.


TodoWrite

Create filesystem router for backend

Create FolderBrowser component

Update CreateProjectModal to use picker


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create filesystem router for backend

Create FolderBrowser component

Update CreateProjectModal to use picker


Now let me create the filesystem router for the remix backend. I'll adapt it from the autocoder-master version.


Write

"""
Filesystem Router
==================

API endpoints for browsing the filesystem for project folder selection.
Provides cross-platform support for Windows, macOS, and Linux.
"""

import logging
import os
import re
import sys
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel

logger = logging.getLogger(__name__)

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

# =============================================================================
# Schemas
# =============================================================================

class DirectoryEntry(BaseModel):
    """Single directory entry."""
    name: str
    path: str
    is_directory: bool
    is_hidden: bool = False
    size: Optional[int] = None
    has_children: bool = False

class DriveInfo(BaseModel):
    """Windows drive information."""
    letter: str
    label: str
    available: bool = True

class DirectoryListResponse(BaseModel):
    """Response for directory listing."""
    current_path: str
    parent_path: Optional[str] = None
    entries: list[DirectoryEntry]
    drives: Optional[list[DriveInfo]] = None

class CreateDirectoryRequest(BaseModel):
    """Request to create a new directory."""
    parent_path: str
    name: str

# =============================================================================
# Platform-Specific Blocked Paths
# =============================================================================

WINDOWS_BLOCKED = {
    "C:\\Windows",
    "C:\\Program Files",
    "C:\\Program Files (x86)",
    "C:\\ProgramData",
    "C:\\System Volume Information",
    "C:\\$Recycle.Bin",
    "C:\\Recovery",
}

MACOS_BLOCKED = {
    "/System",
    "/Library",
    "/private",
    "/usr",
    "/bin",
    "/sbin",
    "/etc",
    "/var",
    "/Volumes",
    "/cores",
    "/opt",
}

LINUX_BLOCKED = {
    "/etc",
    "/var",
    "/usr",
    "/bin",
    "/sbin",
    "/boot",
    "/proc",
    "/sys",
    "/dev",
    "/root",
    "/lib",
    "/lib64",
    "/run",
    "/tmp",
    "/opt",
}

UNIVERSAL_BLOCKED_RELATIVE = {
    ".ssh",
    ".aws",
    ".gnupg",
    ".config/gh",
    ".netrc",
    ".docker",
    ".kube",
    ".terraform",
}

HIDDEN_PATTERNS = [
    r"^\.env",
    r".*\.key$",
    r".*\.pem$",
    r".*credentials.*",
    r".*secrets.*",
]

def get_blocked_paths() -> set[Path]:
    """Get the set of blocked paths for the current platform."""
    home = Path.home()
    blocked = set()

    if sys.platform == "win32":
        for p in WINDOWS_BLOCKED:
            blocked.add(Path(p).resolve())
    elif sys.platform == "darwin":
        for p in MACOS_BLOCKED:
            blocked.add(Path(p).resolve())
    else:
        for p in LINUX_BLOCKED:
            blocked.add(Path(p).resolve())

    for rel in UNIVERSAL_BLOCKED_RELATIVE:
        blocked.add((home / rel).resolve())

    return blocked

def is_path_blocked(path: Path) -> bool:
    """Check if a path is in the blocked list."""
    try:
        resolved = path.resolve()
    except (OSError, ValueError):
        return True

    blocked_paths = get_blocked_paths()

    for blocked in blocked_paths:
        try:
            resolved.relative_to(blocked)
            return True
        except ValueError:
            pass
        if resolved == blocked:
            return True

    return False

def is_hidden_file(path: Path) -> bool:
    """Check if a file/directory is hidden."""
    name = path.name
    if name.startswith('.'):
        return True

    if sys.platform == "win32":
        try:
            import ctypes
            attrs = ctypes.windll.kernel32.GetFileAttributesW(str(path))
            if attrs != -1 and (attrs & 0x02):
                return True
        except Exception:
            pass

    return False

def matches_blocked_pattern(name: str) -> bool:
    """Check if filename matches a blocked pattern."""
    for pattern in HIDDEN_PATTERNS:
        if re.match(pattern, name, re.IGNORECASE):
            return True
    return False

def is_unc_path(path_str: str) -> bool:
    """Check if path is a Windows UNC path."""
    return path_str.startswith("\\\\") or path_str.startswith("//")

def get_windows_drives() -> list[DriveInfo]:
    """Get list of available drives on Windows."""
    drives = []

    try:
        import ctypes
        import string

        bitmask = ctypes.windll.kernel32.GetLogicalDrives()

        for i, letter in enumerate(string.ascii_uppercase):
            if bitmask & (1 << i):
                drive_path = f"{letter}:\\"
                try:
                    volume_name = ctypes.create_unicode_buffer(1024)
                    ctypes.windll.kernel32.GetVolumeInformationW(
                        drive_path,
                        volume_name,
                        1024,
                        None, None, None, None, 0
                    )
                    label = volume_name.value or f"Local Disk ({letter}:)"
                except Exception:
                    label = f"Drive ({letter}:)"

                available = os.path.exists(drive_path)
                drives.append(DriveInfo(
                    letter=letter,
                    label=label,
                    available=available,
                ))
    except Exception:
        drives.append(DriveInfo(letter="C", label="Local Disk (C:)", available=True))

    return drives

# =============================================================================
# Endpoints
# =============================================================================

@router.get("/list", response_model=DirectoryListResponse)
async def list_directory(
    path: Optional[str] = Query(None, description="Directory path to list"),
    show_hidden: bool = Query(False, description="Include hidden files"),
):
    """List contents of a directory."""
    if path is None or path == "":
        target = Path.home()
    else:
        if is_unc_path(path):
            raise HTTPException(status_code=403, detail="Network paths are not allowed")
        target = Path(path)

    try:
        target = target.resolve()
    except (OSError, ValueError) as e:
        raise HTTPException(status_code=400, detail=f"Invalid path: {e}")

    if is_path_blocked(target):
        raise HTTPException(status_code=403, detail="Access to this directory is not allowed")

    if not target.exists():
        raise HTTPException(status_code=404, detail="Directory not found")

    if not target.is_dir():
        raise HTTPException(status_code=400, detail="Path is not a directory")

    if not os.access(target, os.R_OK):
        raise HTTPException(status_code=403, detail="No read permission")

    entries = []
    try:
        for item in sorted(target.iterdir(), key=lambda x: x.name.lower()):
            if matches_blocked_pattern(item.name):
                continue

            hidden = is_hidden_file(item)
            if hidden and not show_hidden:
                continue

            if is_path_blocked(item):
                continue

            if item.is_dir():
                try:
                    has_children = False
                    try:
                        for child in item.iterdir():
                            if child.is_dir() and not is_path_blocked(child):
                                has_children = True
                                break
                    except (PermissionError, OSError):
                        pass

                    entries.append(DirectoryEntry(
                        name=item.name,
                        path=item.as_posix(),
                        is_directory=True,
                        is_hidden=hidden,
                        size=None,
                        has_children=has_children,
                    ))
                except Exception:
                    pass

    except PermissionError:
        raise HTTPException(status_code=403, detail="Permission denied")
    except OSError as e:
        raise HTTPException(status_code=500, detail=f"Error reading directory: {e}")

    parent_path = None
    if target != target.parent:
        parent = target.parent
        if not is_path_blocked(parent):
            parent_path = parent.as_posix()

    drives = None
    if sys.platform == "win32":
        drives = get_windows_drives()

    return DirectoryListResponse(
        current_path=target.as_posix(),
        parent_path=parent_path,
        entries=entries,
        drives=drives,
    )

@router.post("/create-directory")
async def create_directory(request: CreateDirectoryRequest):
    """Create a new directory."""
    name = request.name.strip()
    if not name:
        raise HTTPException(status_code=400, detail="Directory name cannot be empty")

    if name in ('.', '..') or '..' in name:
        raise HTTPException(status_code=400, detail="Invalid directory name")

    invalid_chars = '<>:"/\\|?*' if sys.platform == "win32" else '/'
    if any(c in name for c in invalid_chars):
        raise HTTPException(status_code=400, detail="Directory name contains invalid characters")

    if is_unc_path(request.parent_path):
        raise HTTPException(status_code=403, detail="Network paths are not allowed")

    try:
        parent = Path(request.parent_path).resolve()
    except (OSError, ValueError) as e:
        raise HTTPException(status_code=400, detail=f"Invalid parent path: {e}")

    if is_path_blocked(parent):
        raise HTTPException(status_code=403, detail="Cannot create directory in this location")

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

    if not parent.is_dir():
        raise HTTPException(status_code=400, detail="Parent path is not a directory")

    if not os.access(parent, os.W_OK):
        raise HTTPException(status_code=403, detail="No write permission")

    new_dir = parent / name

    if new_dir.exists():
        raise HTTPException(status_code=409, detail="Directory already exists")

    try:
        new_dir.mkdir(parents=False, exist_ok=False)
    except OSError as e:
        raise HTTPException(status_code=500, detail=f"Failed to create directory: {e}")

    return {
        "success": True,
        "path": new_dir.as_posix(),
    }

@router.get("/home")
async def get_home_directory():
    """Get the user's home directory path."""
    home = Path.home()
    return {
        "path": home.as_posix(),
    }


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\app\routers\filesystem.py

"""
Filesystem Router
==================

API endpoints for browsing the filesystem for project folder selection.
Provides cross-platform support for Windows, macOS, and Linux.
"""

import logging
import os
import re
import sys
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel

logger = logging.getLogger(__name__)

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

# =============================================================================
# Schemas
# =============================================================================

class DirectoryEntry(BaseModel):
    """Single directory entry."""
    name: str
    path: str
    is_directory: bool
    is_hidden: bool = False
    size: Optional[int] = None
    has_children: bool = False

class DriveInfo(BaseModel):
    """Windows drive information."""
    letter: str
    label: str
    available: bool = True

class DirectoryListResponse(BaseModel):
    """Response for directory listing."""
    current_path: str
    parent_path: Optional[str] = None
    entries: list[DirectoryEntry]
    drives: Optional[list[DriveInfo]] = None

class CreateDirectoryRequest(BaseModel):
    """Request to create a new directory."""
    parent_path: str
    name: str

# =============================================================================
# Platform-Specific Blocked Paths
# =============================================================================

WINDOWS_BLOCKED = {
    "C:\\Windows",
    "C:\\Program Files",
    "C:\\Program Files (x86)",
    "C:\\ProgramData",
    "C:\\System Volume Information",
    "C:\\$Recycle.Bin",
    "C:\\Recovery",
}

MACOS_BLOCKED = {
    "/System",
    "/Library",
    "/private",
    "/usr",
    "/bin",
    "/sbin",
    "/etc",
    "/var",
    "/Volumes",
    "/cores",
    "/opt",
}

LINUX_BLOCKED = {
    "/etc",
    "/var",
    "/usr",
    "/bin",
    "/sbin",
    "/boot",
    "/proc",
    "/sys",
    "/dev",
    "/root",
    "/lib",
    "/lib64",
    "/run",
    "/tmp",
    "/opt",
}

UNIVERSAL_BLOCKED_RELATIVE = {
    ".ssh",
    ".aws",
    ".gnupg",
    ".config/gh",
    ".netrc",
    ".docker",
    ".kube",
    ".terraform",
}

HIDDEN_PATTERNS = [
    r"^\.env",
    r".*\.key$",
    r".*\.pem$",
    r".*credentials.*",
    r".*secrets.*",
]

def get_blocked_paths() -> set[Path]:
    """Get the set of blocked paths for the current platform."""
    home = Path.home()
    blocked = set()

    if sys.platform == "win32":
        for p in WINDOWS_BLOCKED:
            blocked.add(Path(p).resolve())
    elif sys.platform == "darwin":
        for p in MACOS_BLOCKED:
            blocked.add(Path(p).resolve())
    else:
        for p in LINUX_BLOCKED:
            blocked.add(Path(p).resolve())

    for rel in UNIVERSAL_BLOCKED_RELATIVE:
        blocked.add((home / rel).resolve())

    return blocked

def is_path_blocked(path: Path) -> bool:
    """Check if a path is in the blocked list."""
    try:
        resolved = path.resolve()
    except (OSError, ValueError):
        return True

    blocked_paths = get_blocked_paths()

    for blocked in blocked_paths:
        try:
            resolved.relative_to(blocked)
            return True
        except ValueError:
            pass
        if resolved == blocked:
            return True

    return False

def is_hidden_file(path: Path) -> bool:
    """Check if a file/directory is hidden."""
    name = path.name
    if name.startswith('.'):
        return True

    if sys.platform == "win32":
        try:
            import ctypes
            attrs = ctypes.windll.kernel32.GetFileAttributesW(str(path))
            if attrs != -1 and (attrs & 0x02):
                return True
        except Exception:
            pass

    return False

def matches_blocked_pattern(name: str) -> bool:
    """Check if filename matches a blocked pattern."""
    for pattern in HIDDEN_PATTERNS:
        if re.match(pattern, name, re.IGNORECASE):
            return True
    return False

def is_unc_path(path_str: str) -> bool:
    """Check if path is a Windows UNC path."""
    return path_str.startswith("\\\\") or path_str.startswith("//")

def get_windows_drives() -> list[DriveInfo]:
    """Get list of available drives on Windows."""
    drives = []

    try:
        import ctypes
        import string

        bitmask = ctypes.windll.kernel32.GetLogicalDrives()

        for i, letter in enumerate(string.ascii_uppercase):
            if bitmask & (1 << i):
                drive_path = f"{letter}:\\"
                try:
                    volume_name = ctypes.create_unicode_buffer(1024)
                    ctypes.windll.kernel32.GetVolumeInformationW(
                        drive_path,
                        volume_name,
                        1024,
                        None, None, None, None, 0
                    )
                    label = volume_name.value or f"Local Disk ({letter}:)"
                except Exception:
                    label = f"Drive ({letter}:)"

                available = os.path.exists(drive_path)
                drives.append(DriveInfo(
                    letter=letter,
                    label=label,
                    available=available,
                ))
    except Exception:
        drives.append(DriveInfo(letter="C", label="Local Disk (C:)", available=True))

    return drives

# =============================================================================
# Endpoints
# =============================================================================

@router.get("/list", response_model=DirectoryListResponse)
async def list_directory(
    path: Optional[str] = Query(None, description="Directory path to list"),
    show_hidden: bool = Query(False, description="Include hidden files"),
):
    """List contents of a directory."""
    if path is None or path == "":
        target = Path.home()
    else:
        if is_unc_path(path):
            raise HTTPException(status_code=403, detail="Network paths are not allowed")
        target = Path(path)

    try:
        target = target.resolve()
    except (OSError, ValueError) as e:
        raise HTTPException(status_code=400, detail=f"Invalid path: {e}")

    if is_path_blocked(target):
        raise HTTPException(status_code=403, detail="Access to this directory is not allowed")

    if not target.exists():
        raise HTTPException(status_code=404, detail="Directory not found")

    if not target.is_dir():
        raise HTTPException(status_code=400, detail="Path is not a directory")

    if not os.access(target, os.R_OK):
        raise HTTPException(status_code=403, detail="No read permission")

    entries = []
    try:
        for item in sorted(target.iterdir(), key=lambda x: x.name.lower()):
            if matches_blocked_pattern(item.name):
                continue

            hidden = is_hidden_file(item)
            if hidden and not show_hidden:
                continue

            if is_path_blocked(item):
                continue

            if item.is_dir():
                try:
                    has_children = False
                    try:
                        for child in item.iterdir():
                            if child.is_dir() and not is_path_blocked(child):
                                has_children = True
                                break
                    except (PermissionError, OSError):
                        pass

                    entries.append(DirectoryEntry(
                        name=item.name,
                        path=item.as_posix(),
                        is_directory=True,
                        is_hidden=hidden,
                        size=None,
                        has_children=has_children,
                    ))
                except Exception:
                    pass

    except PermissionError:
        raise HTTPException(status_code=403, detail="Permission denied")
    except OSError as e:
        raise HTTPException(status_code=500, detail=f"Error reading directory: {e}")

    parent_path = None
    if target != target.parent:
        parent = target.parent
        if not is_path_blocked(parent):
            parent_path = parent.as_posix()

    drives = None
    if sys.platform == "win32":
        drives = get_windows_drives()

    return DirectoryListResponse(
        current_path=target.as_posix(),
        parent_path=parent_path,
        entries=entries,
        drives=drives,
    )

@router.post("/create-directory")
async def create_directory(request: CreateDirectoryRequest):
    """Create a new directory."""
    name = request.name.strip()
    if not name:
        raise HTTPException(status_code=400, detail="Directory name cannot be empty")

    if name in ('.', '..') or '..' in name:
        raise HTTPException(status_code=400, detail="Invalid directory name")

    invalid_chars = '<>:"/\\|?*' if sys.platform == "win32" else '/'
    if any(c in name for c in invalid_chars):
        raise HTTPException(status_code=400, detail="Directory name contains invalid characters")

    if is_unc_path(request.parent_path):
        raise HTTPException(status_code=403, detail="Network paths are not allowed")

    try:
        parent = Path(request.parent_path).resolve()
    except (OSError, ValueError) as e:
        raise HTTPException(status_code=400, detail=f"Invalid parent path: {e}")

    if is_path_blocked(parent):
        raise HTTPException(status_code=403, detail="Cannot create directory in this location")

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

    if not parent.is_dir():
        raise HTTPException(status_code=400, detail="Parent path is not a directory")

    if not os.access(parent, os.W_OK):
        raise HTTPException(status_code=403, detail="No write permission")

    new_dir = parent / name

    if new_dir.exists():
        raise HTTPException(status_code=409, detail="Directory already exists")

    try:
        new_dir.mkdir(parents=False, exist_ok=False)
    except OSError as e:
        raise HTTPException(status_code=500, detail=f"Failed to create directory: {e}")

    return {
        "success": True,
        "path": new_dir.as_posix(),
    }

@router.get("/home")
async def get_home_directory():
    """Get the user's home directory path."""
    home = Path.home()
    return {
        "path": home.as_posix(),
    }


Now I need to register the filesystem router in the main.py and __init__.py.


Now register it in the router init:


     1→"""
     2→API Routers
     3→===========
     4→
     5→FastAPI routers for Autocoder+ API endpoints.
     6→"""
     7→
     8→from .projects import router as projects_router
     9→from .websocket import router as websocket_router
    10→from .agents import router as agents_router
    11→from .features import router as features_router
    12→from .permissions import router as permissions_router
    13→from .spec import router as spec_router
    14→from .infrastructure import router as infrastructure_router
    15→from .git import router as git_router
    16→
    17→__all__ = [
    18→    "projects_router",
    19→    "websocket_router",
    20→    "agents_router",
    21→    "features_router",
    22→    "permissions_router",
    23→    "spec_router",
    24→    "infrastructure_router",
    25→    "git_router",
    26→]
    27→

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

FastAPI routers for Autocoder+ API endpoints.
"""

from .projects import router as projects_router
from .websocket import router as websocket_router
from .agents import router as agents_router
from .features import router as features_router
from .permissions import router as permissions_router
from .spec import router as spec_router
from .infrastructure import router as infrastructure_router
from .git import router as git_router

__all__ = [
    "projects_router",
    "websocket_router",
    "agents_router",
    "features_router",
    "permissions_router",
    "spec_router",
    "infrastructure_router",
    "git_router",
]


Now I need to add the filesystem_router to the __init__.py and main.py.


Edit

from .git import router as git_router

__all__ = [
    "projects_router",
    "websocket_router",
    "agents_router",
    "features_router",
    "permissions_router",
    "spec_router",
    "infrastructure_router",
    "git_router",
]

from .git import router as git_router
from .filesystem import router as filesystem_router

__all__ = [
    "projects_router",
    "websocket_router",
    "agents_router",
    "features_router",
    "permissions_router",
    "spec_router",
    "infrastructure_router",
    "git_router",
    "filesystem_router",
]


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\app\routers\__init__.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
    11→from .features import router as features_router
    12→from .permissions import router as permissions_router
    13→from .spec import router as spec_router
    14→from .infrastructure import router as infrastructure_router
    15→from .git import router as git_router
    16→from .filesystem import router as filesystem_router
    17→
    18→__all__ = [
    19→    "projects_router",
    20→    "websocket_router",
    21→    "agents_router",
    22→    "features_router",
    23→    "permissions_router",
    24→    "spec_router",
    25→    "infrastructure_router",
    26→    "git_router",
    27→    "filesystem_router",
    28→]
    29→

from .git import router as git_router
from .filesystem import router as filesystem_router

__all__ = [
    "projects_router",
    "websocket_router",
    "agents_router",
    "features_router",
    "permissions_router",
    "spec_router",
    "infrastructure_router",
    "git_router",
    "filesystem_router",
]

"""
API Routers
===========

FastAPI routers for Autocoder+ API endpoints.
"""

from .projects import router as projects_router
from .websocket import router as websocket_router
from .agents import router as agents_router
from .features import router as features_router
from .permissions import router as permissions_router
from .spec import router as spec_router
from .infrastructure import router as infrastructure_router
from .git import router as git_router

__all__ = [
    "projects_router",
    "websocket_router",
    "agents_router",
    "features_router",
    "permissions_router",
    "spec_router",
    "infrastructure_router",
    "git_router",
]

+from .filesystem import router as filesystem_router


Now update the main.py to include the filesystem router.


     1→"""
     2→Autocoder+ FastAPI Application
     3→==============================
     4→
     5→Main entry point for the Autocoder+ backend server.
     6→Provides REST API, WebSocket connections, and serves the frontend.
     7→"""
     8→
     9→from contextlib import asynccontextmanager
    10→from datetime import datetime
    11→from pathlib import Path
    12→
    13→from fastapi import FastAPI, HTTPException, Request
    14→from fastapi.middleware.cors import CORSMiddleware
    15→from fastapi.responses import FileResponse
    16→from fastapi.staticfiles import StaticFiles
    17→from sqlalchemy import text
    18→
    19→from .config import settings
    20→from .database import init_db, engine
    21→from .models import *  # Import all models to register them with Base
    22→from .routers import projects_router, websocket_router, agents_router, features_router, permissions_router, spec_router, infrastructure_router, git_router
    23→
    24→# Paths
    25→ROOT_DIR = Path(__file__).parent.parent
    26→FRONTEND_DIST_DIR = ROOT_DIR.parent / "frontend" / "dist"
    27→
    28→
    29→async def run_migrations():
    30→    """Run database migrations for new columns."""
    31→    async with engine.begin() as conn:
    32→        # Check if depends_on column exists in features table
    33→        result = await conn.execute(text("PRAGMA table_info(features)"))
    34→        columns = [row[1] for row in result.fetchall()]
    35→
    36→        if 'depends_on' not in columns:
    37→            print("Adding depends_on column to features table...")
    38→            await conn.execute(text('ALTER TABLE features ADD COLUMN depends_on JSON DEFAULT "[]"'))
    39→            print("depends_on column added successfully")
    40→
    41→        if 'files_modified' not in columns:
    42→            print("Adding files_modified column to features table...")
    43→            await conn.execute(text('ALTER TABLE features ADD COLUMN files_modified JSON DEFAULT "[]"'))
    44→            print("files_modified column added successfully")
    45→
    46→
    47→@asynccontextmanager
    48→async def lifespan(app: FastAPI):
    49→    """Lifespan context manager for startup and shutdown."""
    50→    # Startup
    51→    print(f"Autocoder+ server starting on {settings.host}:{settings.port}")
    52→    print("Initializing database...")
    53→    await init_db()
    54→    print("Database initialized successfully")
    55→    print("Running migrations...")
    56→    await run_migrations()
    57→    print("Migrations complete")
    58→    yield
    59→    # Shutdown
    60→    print("Autocoder+ server shutting down...")
    61→
    62→
    63→# Create FastAPI app
    64→app = FastAPI(
    65→    title="Autocoder+",
    66→    description="Enhanced autonomous coding agent with permission management and dual-agent execution",
    67→    version="1.0.0",
    68→    lifespan=lifespan,
    69→)
    70→
    71→# CORS middleware
    72→app.add_middleware(
    73→    CORSMiddleware,
    74→    allow_origins=settings.allowed_origins,
    75→    allow_credentials=True,
    76→    allow_methods=["*"],
    77→    allow_headers=["*"],
    78→)
    79→
    80→

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

"""
Autocoder+ FastAPI Application
==============================

Main entry point for the Autocoder+ backend server.
Provides REST API, WebSocket connections, and serves the frontend.
"""

from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path

from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import text

from .config import settings
from .database import init_db, engine
from .models import *  # Import all models to register them with Base
from .routers import projects_router, websocket_router, agents_router, features_router, permissions_router, spec_router, infrastructure_router, git_router

# Paths
ROOT_DIR = Path(__file__).parent.parent
FRONTEND_DIST_DIR = ROOT_DIR.parent / "frontend" / "dist"

async def run_migrations():
    """Run database migrations for new columns."""
    async with engine.begin() as conn:
        # Check if depends_on column exists in features table
        result = await conn.execute(text("PRAGMA table_info(features)"))
        columns = [row[1] for row in result.fetchall()]

        if 'depends_on' not in columns:
            print("Adding depends_on column to features table...")
            await conn.execute(text('ALTER TABLE features ADD COLUMN depends_on JSON DEFAULT "[]"'))
            print("depends_on column added successfully")

        if 'files_modified' not in columns:
            print("Adding files_modified column to features table...")
            await conn.execute(text('ALTER TABLE features ADD COLUMN files_modified JSON DEFAULT "[]"'))
            print("files_modified column added successfully")

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan context manager for startup and shutdown."""
    # Startup
    print(f"Autocoder+ server starting on {settings.host}:{settings.port}")
    print("Initializing database...")
    await init_db()
    print("Database initialized successfully")
    print("Running migrations...")
    await run_migrations()
    print("Migrations complete")
    yield
    # Shutdown
    print("Autocoder+ server shutting down...")

# Create FastAPI app
app = FastAPI(
    title="Autocoder+",
    description="Enhanced autonomous coding agent with permission management and dual-agent execution",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


I see the import line at line 22. I need to add `filesystem_router` to that import. Let me also check if the routers are registered later in the file.


from .routers import projects_router, websocket_router, agents_router, features_router, permissions_router, spec_router, infrastructure_router, git_router

from .routers import projects_router, websocket_router, agents_router, features_router, permissions_router, spec_router, infrastructure_router, git_router, filesystem_router


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\app\main.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
    18→
    19→from .config import settings
    20→from .database import init_db, engine
    21→from .models import *  # Import all models to register them with Base
    22→from .routers import projects_router, websocket_router, agents_router, features_router, permissions_router, spec_router, infrastructure_router, git_router, filesystem_router
    23→
    24→# Paths
    25→ROOT_DIR = Path(__file__).parent.parent
    26→FRONTEND_DIST_DIR = ROOT_DIR.parent / "frontend" / "dist"

"""
Autocoder+ FastAPI Application
==============================

Main entry point for the Autocoder+ backend server.
Provides REST API, WebSocket connections, and serves the frontend.
"""

from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path

from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import text

from .config import settings
from .database import init_db, engine
from .models import *  # Import all models to register them with Base
from .routers import projects_router, websocket_router, agents_router, features_router, permissions_router, spec_router, infrastructure_router, git_router

# Paths
ROOT_DIR = Path(__file__).parent.parent
FRONTEND_DIST_DIR = ROOT_DIR.parent / "frontend" / "dist"

async def run_migrations():
    """Run database migrations for new columns."""
    async with engine.begin() as conn:
        # Check if depends_on column exists in features table
        result = await conn.execute(text("PRAGMA table_info(features)"))
        columns = [row[1] for row in result.fetchall()]

        if 'depends_on' not in columns:
            print("Adding depends_on column to features table...")
            await conn.execute(text('ALTER TABLE features ADD COLUMN depends_on JSON DEFAULT "[]"'))
            print("depends_on column added successfully")

        if 'files_modified' not in columns:
            print("Adding files_modified column to features table...")
            await conn.execute(text('ALTER TABLE features ADD COLUMN files_modified JSON DEFAULT "[]"'))
            print("files_modified column added successfully")

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan context manager for startup and shutdown."""
    # Startup
    print(f"Autocoder+ server starting on {settings.host}:{settings.port}")
    print("Initializing database...")
    await init_db()
    print("Database initialized successfully")
    print("Running migrations...")
    await run_migrations()
    print("Migrations complete")
    yield
    # Shutdown
    print("Autocoder+ server shutting down...")

# Create FastAPI app
app = FastAPI(
    title="Autocoder+",
    description="Enhanced autonomous coding agent with permission management and dual-agent execution",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ============================================================================
# Security Middleware
# ============================================================================

@app.middleware("http")
async def require_localhost(request: Request, call_next):
    """Only allow requests from localhost for security."""
    client_host = request.client.host if request.client else None

    # Allow localhost connections
    if client_host not in ("127.0.0.1", "::1", "localhost", None):
        raise HTTPException(status_code=403, detail="Localhost access only")

    return await call_next(request)

# ============================================================================
# Health Check Endpoint
# ============================================================================

@app.get("/health")
async def health_check():
    """Health check endpoint for monitoring server status."""
    return {
        "status": "healthy",
        "service": "autocoder-plus",
        "timestamp": datetime.utcnow().isoformat(),
        "version": "1.0.0"
    }

@app.get("/api/health")
async def api_health_check():
    """Alternative health check endpoint under /api prefix."""
    return await health_check()

# ============================================================================
# Database Debug Endpoint
# ============================================================================

@app.get("/api/debug/tables")
async def get_database_tables():
    """Debug endpoint to list all database tables."""
    async with engine.begin() as conn:
        result = await conn.execute(text("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"))
        tables = [row[0] for row in result.fetchall()]

    return {"tables": tables, "count": len(tables)}

# ============================================================================
# API Routers
# ============================================================================

app.include_router(projects_router)
app.include_router(websocket_router)
app.include_router(agents_router)
app.include_router(features_router)
app.include_router(permissions_router)
app.include_router(spec_router)
app.include_router(infrastructure_router)
app.include_router(git_router)

# ============================================================================
# Static File Serving (Production)
# ============================================================================

# Serve React build files if they exist
if FRONTEND_DIST_DIR.exists():
    # Mount static assets
    assets_dir = FRONTEND_DIST_DIR / "assets"
    if assets_dir.exists():
        app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")

    @app.get("/")
    async def serve_index():
        """Serve the React app index.html."""
        return FileResponse(FRONTEND_DIST_DIR / "index.html")

    @app.get("/{path:path}")
    async def serve_spa(path: str):
        """Serve static files or fall back to index.html for SPA routing."""
        # API routes shouldn't reach here
        if path.startswith("api/") or path.startswith("ws/") or path == "health":
            raise HTTPException(status_code=404)

        # Try to serve the file directly
        file_path = FRONTEND_DIST_DIR / path
        if file_path.exists() and file_path.is_file():
            return FileResponse(file_path)

        # Fall back to index.html for SPA routing
        return FileResponse(FRONTEND_DIST_DIR / "index.html")

# ============================================================================
# Main Entry Point
# ============================================================================

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app.main:app",
        host=settings.host,
        port=settings.port,
        reload=settings.debug,
    )

-from .routers import projects_router, websocket_router, agents_router, features_router, permissions_router, spec_router, infrastructure_router, git_router

+from .routers import projects_router, websocket_router, agents_router, features_router, permissions_router, spec_router, infrastructure_router, git_router, filesystem_router


Now I need to find where the routers are included and add filesystem_router there.


136:app.include_router(projects_router)
137:app.include_router(websocket_router)
138:app.include_router(agents_router)
139:app.include_router(features_router)
140:app.include_router(permissions_router)
141:app.include_router(spec_router)
142:app.include_router(infrastructure_router)
143:app.include_router(git_router)


Good, I need to add the filesystem_router to the list.


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\app\main.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   132→# ============================================================================
   133→# API Routers
   134→# ============================================================================
   135→
   136→app.include_router(projects_router)
   137→app.include_router(websocket_router)
   138→app.include_router(agents_router)
   139→app.include_router(features_router)
   140→app.include_router(permissions_router)
   141→app.include_router(spec_router)
   142→app.include_router(infrastructure_router)
   143→app.include_router(git_router)
   144→app.include_router(filesystem_router)
   145→
   146→
   147→# ============================================================================
   148→# Static File Serving (Production)

"""
Autocoder+ FastAPI Application
==============================

Main entry point for the Autocoder+ backend server.
Provides REST API, WebSocket connections, and serves the frontend.
"""

from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path

from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import text

from .config import settings
from .database import init_db, engine
from .models import *  # Import all models to register them with Base
from .routers import projects_router, websocket_router, agents_router, features_router, permissions_router, spec_router, infrastructure_router, git_router, filesystem_router

# Paths
ROOT_DIR = Path(__file__).parent.parent
FRONTEND_DIST_DIR = ROOT_DIR.parent / "frontend" / "dist"

async def run_migrations():
    """Run database migrations for new columns."""
    async with engine.begin() as conn:
        # Check if depends_on column exists in features table
        result = await conn.execute(text("PRAGMA table_info(features)"))
        columns = [row[1] for row in result.fetchall()]

        if 'depends_on' not in columns:
            print("Adding depends_on column to features table...")
            await conn.execute(text('ALTER TABLE features ADD COLUMN depends_on JSON DEFAULT "[]"'))
            print("depends_on column added successfully")

        if 'files_modified' not in columns:
            print("Adding files_modified column to features table...")
            await conn.execute(text('ALTER TABLE features ADD COLUMN files_modified JSON DEFAULT "[]"'))
            print("files_modified column added successfully")

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan context manager for startup and shutdown."""
    # Startup
    print(f"Autocoder+ server starting on {settings.host}:{settings.port}")
    print("Initializing database...")
    await init_db()
    print("Database initialized successfully")
    print("Running migrations...")
    await run_migrations()
    print("Migrations complete")
    yield
    # Shutdown
    print("Autocoder+ server shutting down...")

# Create FastAPI app
app = FastAPI(
    title="Autocoder+",
    description="Enhanced autonomous coding agent with permission management and dual-agent execution",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ============================================================================
# Security Middleware
# ============================================================================

@app.middleware("http")
async def require_localhost(request: Request, call_next):
    """Only allow requests from localhost for security."""
    client_host = request.client.host if request.client else None

    # Allow localhost connections
    if client_host not in ("127.0.0.1", "::1", "localhost", None):
        raise HTTPException(status_code=403, detail="Localhost access only")

    return await call_next(request)

# ============================================================================
# Health Check Endpoint
# ============================================================================

@app.get("/health")
async def health_check():
    """Health check endpoint for monitoring server status."""
    return {
        "status": "healthy",
        "service": "autocoder-plus",
        "timestamp": datetime.utcnow().isoformat(),
        "version": "1.0.0"
    }

@app.get("/api/health")
async def api_health_check():
    """Alternative health check endpoint under /api prefix."""
    return await health_check()

# ============================================================================
# Database Debug Endpoint
# ============================================================================

@app.get("/api/debug/tables")
async def get_database_tables():
    """Debug endpoint to list all database tables."""
    async with engine.begin() as conn:
        result = await conn.execute(text("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"))
        tables = [row[0] for row in result.fetchall()]

    return {"tables": tables, "count": len(tables)}

# ============================================================================
# API Routers
# ============================================================================

app.include_router(projects_router)
app.include_router(websocket_router)
app.include_router(agents_router)
app.include_router(features_router)
app.include_router(permissions_router)
app.include_router(spec_router)
app.include_router(infrastructure_router)
app.include_router(git_router)

# ============================================================================
# Static File Serving (Production)
# ============================================================================

# Serve React build files if they exist
if FRONTEND_DIST_DIR.exists():
    # Mount static assets
    assets_dir = FRONTEND_DIST_DIR / "assets"
    if assets_dir.exists():
        app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")

    @app.get("/")
    async def serve_index():
        """Serve the React app index.html."""
        return FileResponse(FRONTEND_DIST_DIR / "index.html")

    @app.get("/{path:path}")
    async def serve_spa(path: str):
        """Serve static files or fall back to index.html for SPA routing."""
        # API routes shouldn't reach here
        if path.startswith("api/") or path.startswith("ws/") or path == "health":
            raise HTTPException(status_code=404)

        # Try to serve the file directly
        file_path = FRONTEND_DIST_DIR / path
        if file_path.exists() and file_path.is_file():
            return FileResponse(file_path)

        # Fall back to index.html for SPA routing
        return FileResponse(FRONTEND_DIST_DIR / "index.html")

# ============================================================================
# Main Entry Point
# ============================================================================

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app.main:app",
        host=settings.host,
        port=settings.port,
        reload=settings.debug,
    )


Now I need to create the FolderBrowser component for the frontend. Let me update the todo and create the component.


TodoWrite

Create filesystem router for backend

Create FolderBrowser component

Update CreateProjectModal to use picker


Create filesystem router for backend

Create FolderBrowser component

Update CreateProjectModal to use picker

Create filesystem router for backend

Create FolderBrowser component

Update CreateProjectModal to use picker


Now let me create the FolderBrowser component for the remix frontend.


Write

import { useState, useEffect } from 'react'

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

interface DriveInfo {
  letter: string
  label: string
  available: boolean
}

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

interface FolderBrowserProps {
  onSelect: (path: string) => void
  onCancel: () => void
  initialPath?: string
  mode?: 'create' | 'import'
}

export function FolderBrowser({ onSelect, onCancel, initialPath, mode = 'create' }: FolderBrowserProps) {
  const [currentPath, setCurrentPath] = useState<string | null>(initialPath || null)
  const [entries, setEntries] = useState<DirectoryEntry[]>([])
  const [drives, setDrives] = useState<DriveInfo[] | null>(null)
  const [parentPath, setParentPath] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [newFolderName, setNewFolderName] = useState('')
  const [showNewFolder, setShowNewFolder] = useState(false)
  const [creatingFolder, setCreatingFolder] = useState(false)

  const isImportMode = mode === 'import'

  // Fetch directory listing
  const fetchDirectory = async (path?: string) => {
    setLoading(true)
    setError(null)

    try {
      const url = path
        ? `/api/filesystem/list?path=${encodeURIComponent(path)}`
        : '/api/filesystem/list'

      const res = await fetch(url)
      if (!res.ok) {
        const data = await res.json()
        throw new Error(data.detail || 'Failed to load directory')
      }

      const data: DirectoryListResponse = await res.json()
      setCurrentPath(data.current_path)
      setEntries(data.entries)
      setDrives(data.drives)
      setParentPath(data.parent_path)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error')
    } finally {
      setLoading(false)
    }
  }

  // Load initial directory
  useEffect(() => {
    fetchDirectory(initialPath || undefined)
  }, [initialPath])

  const handleNavigate = (path: string) => {
    fetchDirectory(path)
    setShowNewFolder(false)
    setNewFolderName('')
  }

  const handleDriveSelect = (letter: string) => {
    handleNavigate(`${letter}:/`)
  }

  const handleGoUp = () => {
    if (parentPath) {
      handleNavigate(parentPath)
    }
  }

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

  const handleCreateFolder = async () => {
    if (!newFolderName.trim() || !currentPath) return

    setCreatingFolder(true)
    setError(null)

    try {
      const res = await fetch('/api/filesystem/create-directory', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          parent_path: currentPath,
          name: newFolderName.trim(),
        }),
      })

      if (!res.ok) {
        const data = await res.json()
        throw new Error(data.detail || 'Failed to create folder')
      }

      const data = await res.json()
      // Navigate to the new folder
      handleNavigate(data.path)
      setShowNewFolder(false)
      setNewFolderName('')
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error')
    } finally {
      setCreatingFolder(false)
    }
  }

  // Get breadcrumb segments
  const getBreadcrumbs = () => {
    if (!currentPath) return []

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

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

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

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

    return segments
  }

  const breadcrumbs = getBreadcrumbs()

  return (

      {/* Header with breadcrumbs */}

          <svg className="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
          </svg>

            {isImportMode ? 'Select Existing Project' : 'Select Project Location'}

        {/* Breadcrumb navigation */}

          {parentPath && (
            <button
              onClick={handleGoUp}
              className="p-1 hover:bg-surface-elevated rounded transition-colors"
              title="Go up"
            >
              <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
              </svg>
            </button>
          )}

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

              {index > 0 && (
                <svg className="w-4 h-4 text-text-secondary mx-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
                </svg>
              )}
              <button
                onClick={() => handleNavigate(crumb.path)}
                className={`px-2 py-1 rounded hover:bg-surface-elevated transition-colors ${
                  index === breadcrumbs.length - 1 ? 'font-medium text-accent' : 'text-text-secondary'
                }`}
              >
                {crumb.name}
              </button>

          ))}

      {/* Drives selector (Windows) */}
      {drives && drives.length > 0 && (

            Drives:
            {drives.filter(d => d.available).map((drive) => (
              <button
                key={drive.letter}
                onClick={() => handleDriveSelect(drive.letter)}
                className={`px-2 py-1 text-xs rounded border transition-colors ${
                  currentPath?.startsWith(`${drive.letter}:`)
                    ? 'bg-accent text-white border-accent'
                    : 'border-border hover:bg-surface'
                }`}
              >
                {drive.letter}:
              </button>
            ))}

      )}

      {/* Directory listing */}

        {loading ? (

        ) : error ? (

            <svg className="w-8 h-8 mx-auto mb-2 text-error" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>

{error}

            <button
              onClick={() => fetchDirectory(currentPath || undefined)}
              className="mt-2 text-sm text-accent hover:underline"
            >
              Retry
            </button>

        ) : entries.length === 0 ? (

            <svg className="w-8 h-8 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
            </svg>

No subfolders

You can create a new folder or select this directory.

        ) : (

            {entries.map((entry) => (
              <button
                key={entry.path}
                onClick={() => handleNavigate(entry.path)}
                className="w-full flex items-center gap-2 p-2 rounded hover:bg-surface-elevated transition-colors text-left"
              >
                <svg className="w-5 h-5 text-warning flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
                </svg>
                {entry.name}
                {entry.has_children && (
                  <svg className="w-4 h-4 text-text-secondary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
                  </svg>
                )}
              </button>
            ))}

        )}

        {/* New folder input */}
        {showNewFolder && (

              <svg className="w-5 h-5 text-accent flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
              </svg>
              <input
                type="text"
                value={newFolderName}
                onChange={(e) => setNewFolderName(e.target.value)}
                placeholder="Folder name"
                className="flex-1 px-2 py-1 text-sm bg-surface border border-border rounded focus:outline-none focus:ring-1 focus:ring-accent"
                autoFocus
                onKeyDown={(e) => {
                  if (e.key === 'Enter') handleCreateFolder()
                  if (e.key === 'Escape') {
                    setShowNewFolder(false)
                    setNewFolderName('')
                  }
                }}
              />
              <button
                onClick={handleCreateFolder}
                disabled={!newFolderName.trim() || creatingFolder}
                className="px-2 py-1 text-sm bg-accent text-white rounded hover:bg-accent/80 disabled:opacity-50"
              >
                {creatingFolder ? '...' : 'Create'}
              </button>
              <button
                onClick={() => {
                  setShowNewFolder(false)
                  setNewFolderName('')
                }}
                className="px-2 py-1 text-sm border border-border rounded hover:bg-surface"
              >
                Cancel
              </button>

        )}

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

        {/* Selected path display */}

Selected path:

{currentPath || 'No folder selected'}

        {/* Actions */}

          {!isImportMode && (
            <button
              onClick={() => setShowNewFolder(true)}
              disabled={showNewFolder || loading}
              className="flex items-center gap-1 px-3 py-2 text-sm border border-border rounded hover:bg-surface-elevated transition-colors disabled:opacity-50"
            >
              <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
              </svg>
              New Folder
            </button>
          )}

          <div className={`flex items-center gap-2 ${isImportMode ? 'w-full justify-end' : ''}`}>
            <button
              onClick={onCancel}
              className="px-4 py-2 text-sm border border-border rounded hover:bg-surface-elevated transition-colors"
            >
              Cancel
            </button>
            <button
              onClick={handleSelect}
              disabled={!currentPath}
              className="px-4 py-2 text-sm bg-accent text-white rounded hover:bg-accent/80 transition-colors disabled:opacity-50"
            >
              {isImportMode ? 'Select This Folder' : 'Select Location'}
            </button>

  )
}

export default FolderBrowser