File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\services\file_lock.py

"""
File Lock Manager
=================

Async-safe file locking to prevent concurrent modifications
to the same files by parallel coding agents.
"""

import asyncio
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

@dataclass
class LockInfo:
    """Information about a file lock."""
    file_path: str
    feature_id: int
    agent_id: str
    locked_at: datetime
    expires_at: Optional[datetime] = None

@dataclass
class LockResult:
    """Result of a lock operation."""
    success: bool
    message: str
    lock_info: Optional[LockInfo] = None
    holder_feature_id: Optional[int] = None

class FileLockManager:
    """
    Manage file locks for parallel coding agents.

    This class provides async-safe file locking to prevent multiple
    agents from modifying the same files simultaneously. It supports:

    - Acquiring locks on individual files or directories
    - Lock timeouts to prevent deadlocks
    - Force release for recovery scenarios
    - Pattern-based locking (e.g., lock all files in a directory)

    Thread Safety:
        All operations use asyncio.Lock to ensure thread safety
        in async contexts.
    """

    # Default lock timeout (1 hour)
    DEFAULT_TIMEOUT_SECONDS = 3600

    def __init__(self, project_dir: Optional[Path] = None):
        """
        Initialize the file lock manager.

        Args:
            project_dir: Optional project directory for relative path resolution
        """
        self.project_dir = project_dir
        self._locks: dict[str, LockInfo] = {}
        self._lock = asyncio.Lock()

    def _normalize_path(self, file_path: str) -> str:
        """Normalize a file path for consistent locking."""
        # Convert to forward slashes and lowercase for consistency
        normalized = file_path.replace("\\", "/").lower()

        # Remove leading/trailing slashes
        normalized = normalized.strip("/")

        return normalized

    async def acquire(
        self,
        file_path: str,
        feature_id: int,
        agent_id: str = "default",
        timeout_seconds: Optional[int] = None,
        wait: bool = False,
        wait_timeout: float = 30.0,
    ) -> LockResult:
        """
        Acquire a lock on a file.

        Args:
            file_path: Path to the file to lock (relative to project)
            feature_id: ID of the feature requesting the lock
            agent_id: ID of the agent requesting the lock
            timeout_seconds: Lock timeout in seconds (None for default)
            wait: If True, wait for the lock to become available
            wait_timeout: Maximum time to wait for the lock (if wait=True)

        Returns:
            LockResult indicating success or failure
        """
        normalized = self._normalize_path(file_path)
        timeout = timeout_seconds or self.DEFAULT_TIMEOUT_SECONDS

        start_time = datetime.now()

        while True:
            async with self._lock:
                # Check if already locked
                existing = self._locks.get(normalized)

                if existing:
                    # Check if lock has expired
                    if existing.expires_at and datetime.now() > existing.expires_at:
                        logger.info(
                            f"Lock on {normalized} expired, releasing "
                            f"(was held by feature {existing.feature_id})"
                        )
                        del self._locks[normalized]
                        existing = None

                if existing:
                    # Already locked by same feature - extend the lock
                    if existing.feature_id == feature_id:
                        existing.expires_at = datetime.now() + timedelta(seconds=timeout)
                        logger.debug(f"Extended lock on {normalized} for feature {feature_id}")
                        return LockResult(
                            success=True,
                            message="Lock extended",
                            lock_info=existing,
                        )

                    # Locked by different feature
                    if not wait:
                        return LockResult(
                            success=False,
                            message=f"File locked by feature {existing.feature_id}",
                            holder_feature_id=existing.feature_id,
                        )
                else:
                    # Lock is available - acquire it
                    lock_info = LockInfo(
                        file_path=normalized,
                        feature_id=feature_id,
                        agent_id=agent_id,
                        locked_at=datetime.now(),
                        expires_at=datetime.now() + timedelta(seconds=timeout),
                    )
                    self._locks[normalized] = lock_info

                    logger.debug(f"Acquired lock on {normalized} for feature {feature_id}")
                    return LockResult(
                        success=True,
                        message="Lock acquired",
                        lock_info=lock_info,
                    )

            # If waiting, check timeout and retry
            if wait:
                elapsed = (datetime.now() - start_time).total_seconds()
                if elapsed >= wait_timeout:
                    return LockResult(
                        success=False,
                        message=f"Timeout waiting for lock on {normalized}",
                        holder_feature_id=existing.feature_id if existing else None,
                    )

                # Wait a bit before retrying
                await asyncio.sleep(0.5)
            else:
                break

        # Should not reach here
        return LockResult(success=False, message="Unexpected error")

    async def acquire_multiple(
        self,
        file_paths: list[str],
        feature_id: int,
        agent_id: str = "default",
        timeout_seconds: Optional[int] = None,
    ) -> LockResult:
        """
        Acquire locks on multiple files atomically.

        Either all locks are acquired or none are. This prevents
        partial lock scenarios that could lead to deadlocks.

        Args:
            file_paths: List of file paths to lock
            feature_id: ID of the feature requesting the locks
            agent_id: ID of the agent requesting the locks
            timeout_seconds: Lock timeout in seconds

        Returns:
            LockResult indicating success or failure
        """
        if not file_paths:
            return LockResult(success=True, message="No files to lock")

        timeout = timeout_seconds or self.DEFAULT_TIMEOUT_SECONDS
        normalized_paths = [self._normalize_path(p) for p in file_paths]

        async with self._lock:
            # First, check if all locks are available
            conflicts = []
            for path in normalized_paths:
                existing = self._locks.get(path)
                if existing:
                    # Check expiration
                    if existing.expires_at and datetime.now() > existing.expires_at:
                        continue  # Expired, will be released

                    if existing.feature_id != feature_id:
                        conflicts.append((path, existing.feature_id))

            if conflicts:
                conflict_msg = ", ".join(
                    f"{p} (feature {fid})" for p, fid in conflicts
                )
                return LockResult(
                    success=False,
                    message=f"Conflicts: {conflict_msg}",
                    holder_feature_id=conflicts[0][1],
                )

            # All locks available - acquire them
            lock_time = datetime.now()
            expires_at = lock_time + timedelta(seconds=timeout)

            for path in normalized_paths:
                # Clean up expired locks
                existing = self._locks.get(path)
                if existing and existing.expires_at and datetime.now() > existing.expires_at:
                    del self._locks[path]

                # Acquire lock
                self._locks[path] = LockInfo(
                    file_path=path,
                    feature_id=feature_id,
                    agent_id=agent_id,
                    locked_at=lock_time,
                    expires_at=expires_at,
                )

            logger.debug(
                f"Acquired {len(normalized_paths)} locks for feature {feature_id}"
            )
            return LockResult(
                success=True,
                message=f"Acquired {len(normalized_paths)} locks",
            )

    async def release(
        self,
        file_path: str,
        feature_id: int,
    ) -> LockResult:
        """
        Release a lock on a file.

        Args:
            file_path: Path to the file to unlock
            feature_id: ID of the feature releasing the lock

        Returns:
            LockResult indicating success or failure
        """
        normalized = self._normalize_path(file_path)

        async with self._lock:
            existing = self._locks.get(normalized)

            if not existing:
                return LockResult(
                    success=True,
                    message="Lock not held (already released)",
                )

            if existing.feature_id != feature_id:
                return LockResult(
                    success=False,
                    message=f"Lock held by different feature ({existing.feature_id})",
                    holder_feature_id=existing.feature_id,
                )

            del self._locks[normalized]
            logger.debug(f"Released lock on {normalized} by feature {feature_id}")

            return LockResult(
                success=True,
                message="Lock released",
            )

    async def release_all(
        self,
        feature_id: int,
    ) -> int:
        """
        Release all locks held by a feature.

        Args:
            feature_id: ID of the feature

        Returns:
            Number of locks released
        """
        async with self._lock:
            to_release = [
                path for path, info in self._locks.items()
                if info.feature_id == feature_id
            ]

            for path in to_release:
                del self._locks[path]

            if to_release:
                logger.debug(
                    f"Released {len(to_release)} locks for feature {feature_id}"
                )

            return len(to_release)

    async def force_release(
        self,
        file_path: str,
    ) -> LockResult:
        """
        Force release a lock regardless of owner.

        Use with caution - this is for recovery scenarios only.

        Args:
            file_path: Path to the file to unlock

        Returns:
            LockResult indicating success
        """
        normalized = self._normalize_path(file_path)

        async with self._lock:
            existing = self._locks.get(normalized)

            if existing:
                logger.warning(
                    f"Force releasing lock on {normalized} "
                    f"(was held by feature {existing.feature_id})"
                )
                del self._locks[normalized]
                return LockResult(
                    success=True,
                    message=f"Force released (was held by feature {existing.feature_id})",
                )

            return LockResult(
                success=True,
                message="Lock not held",
            )

    async def is_locked(
        self,
        file_path: str,
    ) -> tuple[bool, Optional[int]]:
        """
        Check if a file is locked.

        Args:
            file_path: Path to check

        Returns:
            Tuple of (is_locked, feature_id or None)
        """
        normalized = self._normalize_path(file_path)

        async with self._lock:
            existing = self._locks.get(normalized)

            if existing:
                # Check expiration
                if existing.expires_at and datetime.now() > existing.expires_at:
                    del self._locks[normalized]
                    return False, None

                return True, existing.feature_id

            return False, None

    async def get_locks_for_feature(
        self,
        feature_id: int,
    ) -> list[LockInfo]:
        """
        Get all locks held by a feature.

        Args:
            feature_id: ID of the feature

        Returns:
            List of lock information
        """
        async with self._lock:
            return [
                info for info in self._locks.values()
                if info.feature_id == feature_id
            ]

    async def get_all_locks(self) -> dict[str, LockInfo]:
        """
        Get all current locks.

        Returns:
            Dictionary mapping file paths to lock information
        """
        async with self._lock:
            # Clean up expired locks first
            now = datetime.now()
            expired = [
                path for path, info in self._locks.items()
                if info.expires_at and now > info.expires_at
            ]
            for path in expired:
                del self._locks[path]

            return dict(self._locks)

    async def cleanup_expired(self) -> int:
        """
        Clean up all expired locks.

        Returns:
            Number of locks cleaned up
        """
        async with self._lock:
            now = datetime.now()
            expired = [
                path for path, info in self._locks.items()
                if info.expires_at and now > info.expires_at
            ]

            for path in expired:
                logger.debug(f"Cleaning up expired lock: {path}")
                del self._locks[path]

            return len(expired)

    def get_stats(self) -> dict:
        """
        Get lock manager statistics (sync version for quick checks).

        Returns:
            Dictionary with lock statistics
        """
        return {
            "total_locks": len(self._locks),
            "locked_files": list(self._locks.keys()),
            "features_with_locks": list(set(
                info.feature_id for info in self._locks.values()
            )),
        }

# Global lock manager instance
_global_lock_manager: Optional[FileLockManager] = None

def get_lock_manager(project_dir: Optional[Path] = None) -> FileLockManager:
    """
    Get or create the global lock manager.

    Args:
        project_dir: Project directory (used on first call)

    Returns:
        The global FileLockManager instance
    """
    global _global_lock_manager

    if _global_lock_manager is None:
        _global_lock_manager = FileLockManager(project_dir)

    return _global_lock_manager

def reset_lock_manager() -> None:
    """Reset the global lock manager (for testing)."""
    global _global_lock_manager
    _global_lock_manager = None


Now let me update the services __init__.py to export these new services, and then create the parallel agent runner.


Now let me update the services init and create the parallel agent runner.


     1→"""
     2→Backend Services
     3→================
     4→
     5→Business logic and process management services.
     6→"""
     7→
     8→from .process_manager import AgentProcessManager
     9→from .credential_resolver import CredentialResolver, get_resolver
    10→from .resource_manager import (
    11→    ResourceManager,
    12→    Resource,
    13→    ResourceScope,
    14→    ResourceType,
    15→    AuthType,
    16→    ResourceAuth,
    17→    VerificationResult,
    18→    get_manager,
    19→)
    20→
    21→__all__ = [
    22→    "AgentProcessManager",
    23→    "CredentialResolver",
    24→    "get_resolver",
    25→    "ResourceManager",
    26→    "Resource",
    27→    "ResourceScope",
    28→    "ResourceType",
    29→    "AuthType",
    30→    "ResourceAuth",
    31→    "VerificationResult",
    32→    "get_manager",
    33→]
    34→

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

"""
Backend Services
================

Business logic and process management services.
"""

from .process_manager import AgentProcessManager
from .credential_resolver import CredentialResolver, get_resolver
from .resource_manager import (
    ResourceManager,
    Resource,
    ResourceScope,
    ResourceType,
    AuthType,
    ResourceAuth,
    VerificationResult,
    get_manager,
)

__all__ = [
    "AgentProcessManager",
    "CredentialResolver",
    "get_resolver",
    "ResourceManager",
    "Resource",
    "ResourceScope",
    "ResourceType",
    "AuthType",
    "ResourceAuth",
    "VerificationResult",
    "get_manager",
]


Let me update the __init__.py to include the new services.


Edit

"""
Backend Services
================

Business logic and process management services.
"""

from .process_manager import AgentProcessManager
from .credential_resolver import CredentialResolver, get_resolver
from .resource_manager import (
    ResourceManager,
    Resource,
    ResourceScope,
    ResourceType,
    AuthType,
    ResourceAuth,
    VerificationResult,
    get_manager,
)

__all__ = [
    "AgentProcessManager",
    "CredentialResolver",
    "get_resolver",
    "ResourceManager",
    "Resource",
    "ResourceScope",
    "ResourceType",
    "AuthType",
    "ResourceAuth",
    "VerificationResult",
    "get_manager",
]

"""
Backend Services
================

Business logic and process management services.
"""

from .process_manager import AgentProcessManager
from .credential_resolver import CredentialResolver, get_resolver
from .resource_manager import (
    ResourceManager,
    Resource,
    ResourceScope,
    ResourceType,
    AuthType,
    ResourceAuth,
    VerificationResult,
    get_manager,
)
from .feature_graph import (
    FeatureGraph,
    ParallelFeature,
    FeatureConflict,
)
from .file_lock import (
    FileLockManager,
    LockInfo,
    LockResult,
    get_lock_manager,
    reset_lock_manager,
)

__all__ = [
    "AgentProcessManager",
    "CredentialResolver",
    "get_resolver",
    "ResourceManager",
    "Resource",
    "ResourceScope",
    "ResourceType",
    "AuthType",
    "ResourceAuth",
    "VerificationResult",
    "get_manager",
    # Feature graph
    "FeatureGraph",
    "ParallelFeature",
    "FeatureConflict",
    # File locking
    "FileLockManager",
    "LockInfo",
    "LockResult",
    "get_lock_manager",
    "reset_lock_manager",
]


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\services\__init__.py has been updated successfully.

"""
Backend Services
================

Business logic and process management services.
"""

from .process_manager import AgentProcessManager
from .credential_resolver import CredentialResolver, get_resolver
from .resource_manager import (
    ResourceManager,
    Resource,
    ResourceScope,
    ResourceType,
    AuthType,
    ResourceAuth,
    VerificationResult,
    get_manager,
)

__all__ = [
    "AgentProcessManager",
    "CredentialResolver",
    "get_resolver",
    "ResourceManager",
    "Resource",
    "ResourceScope",
    "ResourceType",
    "AuthType",
    "ResourceAuth",
    "VerificationResult",
    "get_manager",
]

"""
Backend Services
================

Business logic and process management services.
"""

from .process_manager import AgentProcessManager
from .credential_resolver import CredentialResolver, get_resolver
from .resource_manager import (
    ResourceManager,
    Resource,
    ResourceScope,
    ResourceType,
    AuthType,
    ResourceAuth,
    VerificationResult,
    get_manager,
)
from .feature_graph import (
    FeatureGraph,
    ParallelFeature,
    FeatureConflict,
)
from .file_lock import (
    FileLockManager,
    LockInfo,
    LockResult,
    get_lock_manager,
    reset_lock_manager,
)

__all__ = [
    "AgentProcessManager",
    "CredentialResolver",
    "get_resolver",
    "ResourceManager",
    "Resource",
    "ResourceScope",
    "ResourceType",
    "AuthType",
    "ResourceAuth",
    "VerificationResult",
    "get_manager",
    # Feature graph
    "FeatureGraph",
    "ParallelFeature",
    "FeatureConflict",
    # File locking
    "FileLockManager",
    "LockInfo",
    "LockResult",
    "get_lock_manager",
    "reset_lock_manager",
]

VerificationResult,
get_manager,
)
+from .feature_graph import (
+    FeatureGraph,
+    ParallelFeature,
+    FeatureConflict,
+)
+from .file_lock import (
+    FileLockManager,
+    LockInfo,
+    LockResult,
+    get_lock_manager,
+    reset_lock_manager,
+)
__all__ = [
"AgentProcessManager",

"ResourceAuth",
"VerificationResult",
"get_manager",
+    # Feature graph
+    "FeatureGraph",
+    "ParallelFeature",
+    "FeatureConflict",
+    # File locking
+    "FileLockManager",
+    "LockInfo",
+    "LockResult",
+    "get_lock_manager",
+    "reset_lock_manager",
]


Now let me update the todo and create the parallel agent runner. I'll create a new file called `parallel_agent.py` that contains the logic for running multiple agents in parallel, integrating with the feature graph and file lock manager.


TodoWrite

Phase 1: Foundation (sandbox, credentials, resources, context injection)

Phase 2: Claude Terminal (backend + frontend)

Phase 3: Git Agent (backend + frontend)

Phase 4.1: Create feature_graph.py for dependency tracking

Phase 4.2: Create file_lock.py for concurrent access control

Phase 4.3: Create parallel_agent.py runner

Phase 5: Integration and UI updates


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

Phase 1: Foundation (sandbox, credentials, resources, context injection)

Phase 2: Claude Terminal (backend + frontend)

Phase 3: Git Agent (backend + frontend)

Phase 4.1: Create feature_graph.py for dependency tracking

Phase 4.2: Create file_lock.py for concurrent access control

Phase 4.3: Modify agent.py for parallel execution

Phase 5: Integration and UI updates

Phase 3: Git Agent (backend + frontend)

Phase 4.3: Create parallel_agent.py runner

Phase 5: Integration and UI updates


Now let me create the parallel agent runner. This will be a new file that extends the existing agent.py functionality to support running multiple agents in parallel.


Write

"""
Parallel Agent Runner
=====================

Run multiple coding agents in parallel on non-conflicting features.
Uses the FeatureGraph to determine which features can be parallelized
and FileLockManager to prevent concurrent file modifications.
"""

import asyncio
import io
import sys
import logging
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Optional, Callable, Any

# Fix Windows console encoding
if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

from claude_agent_sdk import ClaudeSDKClient

from client import create_client
from progress import has_features, print_progress_summary, get_feature_stats
from prompts import get_coding_prompt, get_coding_prompt_yolo, copy_spec_to_project, get_initializer_prompt

# Import server services
import sys as _sys
_root = Path(__file__).parent
if str(_root / "server") not in _sys.path:
    _sys.path.insert(0, str(_root))

from server.services.feature_graph import FeatureGraph, ParallelFeature
from server.services.file_lock import FileLockManager, get_lock_manager
from server.agents.git_agent import GitAgent, GitAgentConfig

logger = logging.getLogger(__name__)

# Configuration
DEFAULT_MAX_PARALLEL = 3
AUTO_CONTINUE_DELAY_SECONDS = 3

@dataclass
class AgentResult:
    """Result of a single agent run."""
    feature_id: int
    feature_name: str
    success: bool
    error: Optional[str] = None
    files_touched: list[str] = field(default_factory=list)
    duration_seconds: float = 0.0

@dataclass
class ParallelRunResult:
    """Result of a parallel agent run."""
    total_features: int
    completed: int
    failed: int
    results: list[AgentResult] = field(default_factory=list)
    duration_seconds: float = 0.0

class ParallelAgentRunner:
    """
    Orchestrate multiple coding agents running in parallel.

    This class:
    1. Uses FeatureGraph to find parallelizable features
    2. Manages file locks to prevent conflicts
    3. Coordinates with GitAgent for commits
    4. Tracks progress and handles failures
    """

    def __init__(
        self,
        project_dir: Path,
        model: str = "claude-sonnet-4-20250514",
        max_parallel: int = DEFAULT_MAX_PARALLEL,
        yolo_mode: bool = False,
        on_feature_start: Optional[Callable[[int, str], Any]] = None,
        on_feature_complete: Optional[Callable[[AgentResult], Any]] = None,
        on_progress: Optional[Callable[[int, int, int], Any]] = None,
    ):
        """
        Initialize the parallel agent runner.

        Args:
            project_dir: Path to the project directory
            model: Claude model to use
            max_parallel: Maximum number of parallel agents
            yolo_mode: If True, skip browser testing
            on_feature_start: Callback when a feature starts (id, name)
            on_feature_complete: Callback when a feature completes
            on_progress: Callback for progress updates (completed, failed, total)
        """
        self.project_dir = project_dir
        self.model = model
        self.max_parallel = max_parallel
        self.yolo_mode = yolo_mode

        # Callbacks
        self._on_feature_start = on_feature_start
        self._on_feature_complete = on_feature_complete
        self._on_progress = on_progress

        # Services
        self.feature_graph = FeatureGraph(project_dir)
        self.lock_manager = FileLockManager(project_dir)
        self.git_agent = GitAgent(
            project_dir,
            GitAgentConfig(
                auto_commit=True,
                auto_push=False,  # Don't auto-push, let user control
                commit_style="conventional",
            ),
        )

        # State
        self._running = False
        self._active_agents: dict[int, asyncio.Task] = {}
        self._results: list[AgentResult] = []

    async def run_single_feature(
        self,
        feature: ParallelFeature,
        agent_id: str,
    ) -> AgentResult:
        """
        Run a single feature with its own agent instance.

        Args:
            feature: The feature to implement
            agent_id: Unique identifier for this agent instance

        Returns:
            AgentResult with success/failure info
        """
        start_time = datetime.now()
        files_touched: list[str] = []

        logger.info(f"[Agent {agent_id}] Starting feature {feature.id}: {feature.name}")

        if self._on_feature_start:
            self._on_feature_start(feature.id, feature.name)

        try:
            # Predict and lock files this feature might touch
            predicted_files = self.feature_graph.predict_files_for_feature(
                feature.id,
                feature.category,
                feature.name,
            )

            if predicted_files:
                lock_result = await self.lock_manager.acquire_multiple(
                    predicted_files,
                    feature.id,
                    agent_id,
                )
                if not lock_result.success:
                    logger.warning(
                        f"[Agent {agent_id}] Could not acquire locks: {lock_result.message}"
                    )
                    # Continue anyway - locks are advisory

            # Create client for this agent
            client = create_client(
                self.project_dir,
                self.model,
                yolo_mode=self.yolo_mode,
            )

            # Generate prompt for this specific feature
            prompt = self._generate_feature_prompt(feature)

            # Run the agent session
            async with client:
                await client.query(prompt)

                async for msg in client.receive_response():
                    msg_type = type(msg).__name__

                    if msg_type == "AssistantMessage" and hasattr(msg, "content"):
                        for block in msg.content:
                            block_type = type(block).__name__

                            if block_type == "TextBlock" and hasattr(block, "text"):
                                # Log output (truncated)
                                text = block.text[:200] + "..." if len(block.text) > 200 else block.text
                                logger.debug(f"[Agent {agent_id}] {text}")

                            elif block_type == "ToolUseBlock" and hasattr(block, "name"):
                                tool_name = block.name
                                logger.info(f"[Agent {agent_id}] [Tool: {tool_name}]")

                                # Track file operations
                                if tool_name in ("Write", "Edit", "Read") and hasattr(block, "input"):
                                    file_path = block.input.get("file_path", "")
                                    if file_path:
                                        files_touched.append(file_path)

            # Record files touched
            if files_touched:
                self.feature_graph.record_files_touched(feature.id, files_touched)

                # Track changes for git
                for file in files_touched:
                    await self.git_agent.track_change(
                        file=file,
                        agent_id=agent_id,
                        feature_id=feature.id,
                        change_type="modified",
                    )

            duration = (datetime.now() - start_time).total_seconds()

            result = AgentResult(
                feature_id=feature.id,
                feature_name=feature.name,
                success=True,
                files_touched=files_touched,
                duration_seconds=duration,
            )

            logger.info(
                f"[Agent {agent_id}] Completed feature {feature.id} "
                f"in {duration:.1f}s ({len(files_touched)} files)"
            )

            return result

        except Exception as e:
            duration = (datetime.now() - start_time).total_seconds()
            logger.error(f"[Agent {agent_id}] Error on feature {feature.id}: {e}")

            return AgentResult(
                feature_id=feature.id,
                feature_name=feature.name,
                success=False,
                error=str(e),
                files_touched=files_touched,
                duration_seconds=duration,
            )

        finally:
            # Release locks
            await self.lock_manager.release_all(feature.id)

            # Notify git agent of feature completion
            await self.git_agent.on_feature_complete(feature.id, feature.name)

    def _generate_feature_prompt(self, feature: ParallelFeature) -> str:
        """
        Generate a prompt for implementing a specific feature.

        Args:
            feature: The feature to implement

        Returns:
            Prompt string
        """
        # Start with the base coding prompt
        if self.yolo_mode:
            base_prompt = get_coding_prompt_yolo(self.project_dir)
        else:
            base_prompt = get_coding_prompt(self.project_dir)

        # Add feature-specific context
        feature_context = f"""
## Current Feature Assignment

You are specifically working on this feature:

**Feature #{feature.id}**: {feature.name}
**Category**: {feature.category}
**Priority**: {feature.priority}

**Description**:
{feature.description}

**Steps**:
{chr(10).join(f"- {step}" for step in feature.steps)}

Focus on implementing this feature completely before moving on.
Use the feature MCP tools to mark this feature as passing when done.
"""

        return base_prompt + "\n\n" + feature_context

    async def run_parallel(
        self,
        max_iterations: Optional[int] = None,
    ) -> ParallelRunResult:
        """
        Run the parallel agent loop.

        Args:
            max_iterations: Maximum number of iteration cycles (None for unlimited)

        Returns:
            ParallelRunResult with overall statistics
        """
        start_time = datetime.now()
        self._running = True
        self._results = []
        iteration = 0

        print("\n" + "=" * 70)
        print("  PARALLEL CODING AGENTS")
        print("=" * 70)
        print(f"\nProject directory: {self.project_dir}")
        print(f"Model: {self.model}")
        print(f"Max parallel agents: {self.max_parallel}")
        print(f"Mode: {'YOLO' if self.yolo_mode else 'Standard'}")
        print()

        # Check if this is a fresh start
        is_first_run = not has_features(self.project_dir)

        if is_first_run:
            print("Fresh start - running initializer first...")
            await self._run_initializer()

        try:
            while self._running:
                iteration += 1

                if max_iterations and iteration > max_iterations:
                    print(f"\nReached max iterations ({max_iterations})")
                    break

                # Get parallelizable features
                features = self.feature_graph.get_parallelizable_features(
                    count=self.max_parallel,
                )

                if not features:
                    # Check if all done
                    stats = get_feature_stats(self.project_dir)
                    if stats and stats.get("pending", 0) == 0 and stats.get("in_progress", 0) == 0:
                        print("\n All features completed!")
                        break
                    else:
                        print("\nNo parallelizable features available, waiting...")
                        await asyncio.sleep(5)
                        continue

                print(f"\n--- Iteration {iteration} ---")
                print(f"Running {len(features)} features in parallel:")
                for f in features:
                    print(f"  - #{f.id}: {f.name}")

                # Create tasks for each feature
                tasks = []
                for i, feature in enumerate(features):
                    agent_id = f"agent-{iteration}-{i}"
                    task = asyncio.create_task(
                        self.run_single_feature(feature, agent_id)
                    )
                    self._active_agents[feature.id] = task
                    tasks.append((feature.id, task))

                # Wait for all tasks to complete
                for feature_id, task in tasks:
                    try:
                        result = await task
                        self._results.append(result)

                        if self._on_feature_complete:
                            self._on_feature_complete(result)

                    except Exception as e:
                        logger.error(f"Task error for feature {feature_id}: {e}")
                        self._results.append(AgentResult(
                            feature_id=feature_id,
                            feature_name="Unknown",
                            success=False,
                            error=str(e),
                        ))

                    finally:
                        self._active_agents.pop(feature_id, None)

                # Progress update
                completed = sum(1 for r in self._results if r.success)
                failed = sum(1 for r in self._results if not r.success)

                if self._on_progress:
                    self._on_progress(completed, failed, len(self._results))

                print_progress_summary(self.project_dir)

                # Brief delay between iterations
                await asyncio.sleep(AUTO_CONTINUE_DELAY_SECONDS)

        except KeyboardInterrupt:
            print("\n\nInterrupted by user")
            self._running = False

        finally:
            # Clean up any remaining tasks
            for task in self._active_agents.values():
                task.cancel()

            # Final git commit for any remaining work
            await self.git_agent.on_session_end()

        # Calculate final results
        duration = (datetime.now() - start_time).total_seconds()
        completed = sum(1 for r in self._results if r.success)
        failed = sum(1 for r in self._results if not r.success)

        return ParallelRunResult(
            total_features=len(self._results),
            completed=completed,
            failed=failed,
            results=self._results,
            duration_seconds=duration,
        )

    async def _run_initializer(self) -> None:
        """Run the initializer agent to create features."""
        print("\n" + "=" * 70)
        print("  INITIALIZER SESSION")
        print("=" * 70)
        print("\nNote: This may take 10-20+ minutes to generate features.")
        print()

        # Copy spec to project
        copy_spec_to_project(self.project_dir)

        # Create client
        client = create_client(self.project_dir, self.model, yolo_mode=False)

        # Get initializer prompt
        prompt = get_initializer_prompt(self.project_dir)

        async with client:
            await client.query(prompt)

            async for msg in client.receive_response():
                msg_type = type(msg).__name__

                if msg_type == "AssistantMessage" and hasattr(msg, "content"):
                    for block in msg.content:
                        block_type = type(block).__name__

                        if block_type == "TextBlock" and hasattr(block, "text"):
                            print(block.text, end="", flush=True)
                        elif block_type == "ToolUseBlock" and hasattr(block, "name"):
                            print(f"\n[Tool: {block.name}]", flush=True)

        print("\n\nInitializer complete!")
        print_progress_summary(self.project_dir)

    def stop(self) -> None:
        """Stop the parallel agent runner."""
        self._running = False
        for task in self._active_agents.values():
            task.cancel()

    def get_active_features(self) -> list[int]:
        """Get list of feature IDs currently being worked on."""
        return list(self._active_agents.keys())

    def get_results(self) -> list[AgentResult]:
        """Get all results so far."""
        return list(self._results)

async def run_parallel_agents(
    project_dir: Path,
    model: str = "claude-sonnet-4-20250514",
    max_parallel: int = DEFAULT_MAX_PARALLEL,
    max_iterations: Optional[int] = None,
    yolo_mode: bool = False,
) -> ParallelRunResult:
    """
    Convenience function to run parallel agents.

    Args:
        project_dir: Path to the project directory
        model: Claude model to use
        max_parallel: Maximum number of parallel agents
        max_iterations: Maximum iteration cycles (None for unlimited)
        yolo_mode: If True, skip browser testing

    Returns:
        ParallelRunResult with statistics
    """
    runner = ParallelAgentRunner(
        project_dir=project_dir,
        model=model,
        max_parallel=max_parallel,
        yolo_mode=yolo_mode,
    )

    return await runner.run_parallel(max_iterations=max_iterations)

# CLI entry point
if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Run parallel coding agents")
    parser.add_argument(
        "--project-dir",
        type=str,
        required=True,
        help="Project directory path or registered name",
    )
    parser.add_argument(
        "--model",
        type=str,
        default="claude-sonnet-4-20250514",
        help="Claude model to use",
    )
    parser.add_argument(
        "--max-parallel",
        type=int,
        default=DEFAULT_MAX_PARALLEL,
        help="Maximum number of parallel agents",
    )
    parser.add_argument(
        "--max-iterations",
        type=int,
        default=None,
        help="Maximum iteration cycles",
    )
    parser.add_argument(
        "--yolo",
        action="store_true",
        help="Enable YOLO mode (skip browser testing)",
    )

    args = parser.parse_args()

    # Resolve project directory
    project_path = Path(args.project_dir)
    if not project_path.is_absolute():
        # Try to resolve from registry
        try:
            from registry import get_project_path
            resolved = get_project_path(args.project_dir)
            if resolved:
                project_path = resolved
        except ImportError:
            pass

    if not project_path.exists():
        project_path.mkdir(parents=True, exist_ok=True)

    # Run
    result = asyncio.run(run_parallel_agents(
        project_dir=project_path,
        model=args.model,
        max_parallel=args.max_parallel,
        max_iterations=args.max_iterations,
        yolo_mode=args.yolo,
    ))

    # Print summary
    print("\n" + "=" * 70)
    print("  PARALLEL RUN COMPLETE")
    print("=" * 70)
    print(f"\nTotal features processed: {result.total_features}")
    print(f"Completed: {result.completed}")
    print(f"Failed: {result.failed}")
    print(f"Duration: {result.duration_seconds:.1f}s")

    if result.failed > 0:
        print("\nFailed features:")
        for r in result.results:
            if not r.success:
                print(f"  - #{r.feature_id}: {r.feature_name}")
                print(f"    Error: {r.error}")


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\parallel_agent.py

"""
Parallel Agent Runner
=====================

Run multiple coding agents in parallel on non-conflicting features.
Uses the FeatureGraph to determine which features can be parallelized
and FileLockManager to prevent concurrent file modifications.
"""

import asyncio
import io
import sys
import logging
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Optional, Callable, Any

# Fix Windows console encoding
if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

from claude_agent_sdk import ClaudeSDKClient

from client import create_client
from progress import has_features, print_progress_summary, get_feature_stats
from prompts import get_coding_prompt, get_coding_prompt_yolo, copy_spec_to_project, get_initializer_prompt

# Import server services
import sys as _sys
_root = Path(__file__).parent
if str(_root / "server") not in _sys.path:
    _sys.path.insert(0, str(_root))

from server.services.feature_graph import FeatureGraph, ParallelFeature
from server.services.file_lock import FileLockManager, get_lock_manager
from server.agents.git_agent import GitAgent, GitAgentConfig

logger = logging.getLogger(__name__)

# Configuration
DEFAULT_MAX_PARALLEL = 3
AUTO_CONTINUE_DELAY_SECONDS = 3

@dataclass
class AgentResult:
    """Result of a single agent run."""
    feature_id: int
    feature_name: str
    success: bool
    error: Optional[str] = None
    files_touched: list[str] = field(default_factory=list)
    duration_seconds: float = 0.0

@dataclass
class ParallelRunResult:
    """Result of a parallel agent run."""
    total_features: int
    completed: int
    failed: int
    results: list[AgentResult] = field(default_factory=list)
    duration_seconds: float = 0.0

class ParallelAgentRunner:
    """
    Orchestrate multiple coding agents running in parallel.

    This class:
    1. Uses FeatureGraph to find parallelizable features
    2. Manages file locks to prevent conflicts
    3. Coordinates with GitAgent for commits
    4. Tracks progress and handles failures
    """

    def __init__(
        self,
        project_dir: Path,
        model: str = "claude-sonnet-4-20250514",
        max_parallel: int = DEFAULT_MAX_PARALLEL,
        yolo_mode: bool = False,
        on_feature_start: Optional[Callable[[int, str], Any]] = None,
        on_feature_complete: Optional[Callable[[AgentResult], Any]] = None,
        on_progress: Optional[Callable[[int, int, int], Any]] = None,
    ):
        """
        Initialize the parallel agent runner.

        Args:
            project_dir: Path to the project directory
            model: Claude model to use
            max_parallel: Maximum number of parallel agents
            yolo_mode: If True, skip browser testing
            on_feature_start: Callback when a feature starts (id, name)
            on_feature_complete: Callback when a feature completes
            on_progress: Callback for progress updates (completed, failed, total)
        """
        self.project_dir = project_dir
        self.model = model
        self.max_parallel = max_parallel
        self.yolo_mode = yolo_mode

        # Callbacks
        self._on_feature_start = on_feature_start
        self._on_feature_complete = on_feature_complete
        self._on_progress = on_progress

        # Services
        self.feature_graph = FeatureGraph(project_dir)
        self.lock_manager = FileLockManager(project_dir)
        self.git_agent = GitAgent(
            project_dir,
            GitAgentConfig(
                auto_commit=True,
                auto_push=False,  # Don't auto-push, let user control
                commit_style="conventional",
            ),
        )

        # State
        self._running = False
        self._active_agents: dict[int, asyncio.Task] = {}
        self._results: list[AgentResult] = []

    async def run_single_feature(
        self,
        feature: ParallelFeature,
        agent_id: str,
    ) -> AgentResult:
        """
        Run a single feature with its own agent instance.

        Args:
            feature: The feature to implement
            agent_id: Unique identifier for this agent instance

        Returns:
            AgentResult with success/failure info
        """
        start_time = datetime.now()
        files_touched: list[str] = []

        logger.info(f"[Agent {agent_id}] Starting feature {feature.id}: {feature.name}")

        if self._on_feature_start:
            self._on_feature_start(feature.id, feature.name)

        try:
            # Predict and lock files this feature might touch
            predicted_files = self.feature_graph.predict_files_for_feature(
                feature.id,
                feature.category,
                feature.name,
            )

            if predicted_files:
                lock_result = await self.lock_manager.acquire_multiple(
                    predicted_files,
                    feature.id,
                    agent_id,
                )
                if not lock_result.success:
                    logger.warning(
                        f"[Agent {agent_id}] Could not acquire locks: {lock_result.message}"
                    )
                    # Continue anyway - locks are advisory

            # Create client for this agent
            client = create_client(
                self.project_dir,
                self.model,
                yolo_mode=self.yolo_mode,
            )

            # Generate prompt for this specific feature
            prompt = self._generate_feature_prompt(feature)

            # Run the agent session
            async with client:
                await client.query(prompt)

                async for msg in client.receive_response():
                    msg_type = type(msg).__name__

                    if msg_type == "AssistantMessage" and hasattr(msg, "content"):
                        for block in msg.content:
                            block_type = type(block).__name__

                            if block_type == "TextBlock" and hasattr(block, "text"):
                                # Log output (truncated)
                                text = block.text[:200] + "..." if len(block.text) > 200 else block.text
                                logger.debug(f"[Agent {agent_id}] {text}")

                            elif block_type == "ToolUseBlock" and hasattr(block, "name"):
                                tool_name = block.name
                                logger.info(f"[Agent {agent_id}] [Tool: {tool_name}]")

                                # Track file operations
                                if tool_name in ("Write", "Edit", "Read") and hasattr(block, "input"):
                                    file_path = block.input.get("file_path", "")
                                    if file_path:
                                        files_touched.append(file_path)

            # Record files touched
            if files_touched:
                self.feature_graph.record_files_touched(feature.id, files_touched)

                # Track changes for git
                for file in files_touched:
                    await self.git_agent.track_change(
                        file=file,
                        agent_id=agent_id,
                        feature_id=feature.id,
                        change_type="modified",
                    )

            duration = (datetime.now() - start_time).total_seconds()

            result = AgentResult(
                feature_id=feature.id,
                feature_name=feature.name,
                success=True,
                files_touched=files_touched,
                duration_seconds=duration,
            )

            logger.info(
                f"[Agent {agent_id}] Completed feature {feature.id} "
                f"in {duration:.1f}s ({len(files_touched)} files)"
            )

            return result

        except Exception as e:
            duration = (datetime.now() - start_time).total_seconds()
            logger.error(f"[Agent {agent_id}] Error on feature {feature.id}: {e}")

            return AgentResult(
                feature_id=feature.id,
                feature_name=feature.name,
                success=False,
                error=str(e),
                files_touched=files_touched,
                duration_seconds=duration,
            )

        finally:
            # Release locks
            await self.lock_manager.release_all(feature.id)

            # Notify git agent of feature completion
            await self.git_agent.on_feature_complete(feature.id, feature.name)

    def _generate_feature_prompt(self, feature: ParallelFeature) -> str:
        """
        Generate a prompt for implementing a specific feature.

        Args:
            feature: The feature to implement

        Returns:
            Prompt string
        """
        # Start with the base coding prompt
        if self.yolo_mode:
            base_prompt = get_coding_prompt_yolo(self.project_dir)
        else:
            base_prompt = get_coding_prompt(self.project_dir)

        # Add feature-specific context
        feature_context = f"""
## Current Feature Assignment

You are specifically working on this feature:

**Feature #{feature.id}**: {feature.name}
**Category**: {feature.category}
**Priority**: {feature.priority}

**Description**:
{feature.description}

**Steps**:
{chr(10).join(f"- {step}" for step in feature.steps)}

Focus on implementing this feature completely before moving on.
Use the feature MCP tools to mark this feature as passing when done.
"""

        return base_prompt + "\n\n" + feature_context

    async def run_parallel(
        self,
        max_iterations: Optional[int] = None,
    ) -> ParallelRunResult:
        """
        Run the parallel agent loop.

        Args:
            max_iterations: Maximum number of iteration cycles (None for unlimited)

        Returns:
            ParallelRunResult with overall statistics
        """
        start_time = datetime.now()
        self._running = True
        self._results = []
        iteration = 0

        print("\n" + "=" * 70)
        print("  PARALLEL CODING AGENTS")
        print("=" * 70)
        print(f"\nProject directory: {self.project_dir}")
        print(f"Model: {self.model}")
        print(f"Max parallel agents: {self.max_parallel}")
        print(f"Mode: {'YOLO' if self.yolo_mode else 'Standard'}")
        print()

        # Check if this is a fresh start
        is_first_run = not has_features(self.project_dir)

        if is_first_run:
            print("Fresh start - running initializer first...")
            await self._run_initializer()

        try:
            while self._running:
                iteration += 1

                if max_iterations and iteration > max_iterations:
                    print(f"\nReached max iterations ({max_iterations})")
                    break

                # Get parallelizable features
                features = self.feature_graph.get_parallelizable_features(
                    count=self.max_parallel,
                )

                if not features:
                    # Check if all done
                    stats = get_feature_stats(self.project_dir)
                    if stats and stats.get("pending", 0) == 0 and stats.get("in_progress", 0) == 0:
                        print("\n All features completed!")
                        break
                    else:
                        print("\nNo parallelizable features available, waiting...")
                        await asyncio.sleep(5)
                        continue

                print(f"\n--- Iteration {iteration} ---")
                print(f"Running {len(features)} features in parallel:")
                for f in features:
                    print(f"  - #{f.id}: {f.name}")

                # Create tasks for each feature
                tasks = []
                for i, feature in enumerate(features):
                    agent_id = f"agent-{iteration}-{i}"
                    task = asyncio.create_task(
                        self.run_single_feature(feature, agent_id)
                    )
                    self._active_agents[feature.id] = task
                    tasks.append((feature.id, task))

                # Wait for all tasks to complete
                for feature_id, task in tasks:
                    try:
                        result = await task
                        self._results.append(result)

                        if self._on_feature_complete:
                            self._on_feature_complete(result)

                    except Exception as e:
                        logger.error(f"Task error for feature {feature_id}: {e}")
                        self._results.append(AgentResult(
                            feature_id=feature_id,
                            feature_name="Unknown",
                            success=False,
                            error=str(e),
                        ))

                    finally:
                        self._active_agents.pop(feature_id, None)

                # Progress update
                completed = sum(1 for r in self._results if r.success)
                failed = sum(1 for r in self._results if not r.success)

                if self._on_progress:
                    self._on_progress(completed, failed, len(self._results))

                print_progress_summary(self.project_dir)

                # Brief delay between iterations
                await asyncio.sleep(AUTO_CONTINUE_DELAY_SECONDS)

        except KeyboardInterrupt:
            print("\n\nInterrupted by user")
            self._running = False

        finally:
            # Clean up any remaining tasks
            for task in self._active_agents.values():
                task.cancel()

            # Final git commit for any remaining work
            await self.git_agent.on_session_end()

        # Calculate final results
        duration = (datetime.now() - start_time).total_seconds()
        completed = sum(1 for r in self._results if r.success)
        failed = sum(1 for r in self._results if not r.success)

        return ParallelRunResult(
            total_features=len(self._results),
            completed=completed,
            failed=failed,
            results=self._results,
            duration_seconds=duration,
        )

    async def _run_initializer(self) -> None:
        """Run the initializer agent to create features."""
        print("\n" + "=" * 70)
        print("  INITIALIZER SESSION")
        print("=" * 70)
        print("\nNote: This may take 10-20+ minutes to generate features.")
        print()

        # Copy spec to project
        copy_spec_to_project(self.project_dir)

        # Create client
        client = create_client(self.project_dir, self.model, yolo_mode=False)

        # Get initializer prompt
        prompt = get_initializer_prompt(self.project_dir)

        async with client:
            await client.query(prompt)

            async for msg in client.receive_response():
                msg_type = type(msg).__name__

                if msg_type == "AssistantMessage" and hasattr(msg, "content"):
                    for block in msg.content:
                        block_type = type(block).__name__

                        if block_type == "TextBlock" and hasattr(block, "text"):
                            print(block.text, end="", flush=True)
                        elif block_type == "ToolUseBlock" and hasattr(block, "name"):
                            print(f"\n[Tool: {block.name}]", flush=True)

        print("\n\nInitializer complete!")
        print_progress_summary(self.project_dir)

    def stop(self) -> None:
        """Stop the parallel agent runner."""
        self._running = False
        for task in self._active_agents.values():
            task.cancel()

    def get_active_features(self) -> list[int]:
        """Get list of feature IDs currently being worked on."""
        return list(self._active_agents.keys())

    def get_results(self) -> list[AgentResult]:
        """Get all results so far."""
        return list(self._results)

async def run_parallel_agents(
    project_dir: Path,
    model: str = "claude-sonnet-4-20250514",
    max_parallel: int = DEFAULT_MAX_PARALLEL,
    max_iterations: Optional[int] = None,
    yolo_mode: bool = False,
) -> ParallelRunResult:
    """
    Convenience function to run parallel agents.

    Args:
        project_dir: Path to the project directory
        model: Claude model to use
        max_parallel: Maximum number of parallel agents
        max_iterations: Maximum iteration cycles (None for unlimited)
        yolo_mode: If True, skip browser testing

    Returns:
        ParallelRunResult with statistics
    """
    runner = ParallelAgentRunner(
        project_dir=project_dir,
        model=model,
        max_parallel=max_parallel,
        yolo_mode=yolo_mode,
    )

    return await runner.run_parallel(max_iterations=max_iterations)

# CLI entry point
if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Run parallel coding agents")
    parser.add_argument(
        "--project-dir",
        type=str,
        required=True,
        help="Project directory path or registered name",
    )
    parser.add_argument(
        "--model",
        type=str,
        default="claude-sonnet-4-20250514",
        help="Claude model to use",
    )
    parser.add_argument(
        "--max-parallel",
        type=int,
        default=DEFAULT_MAX_PARALLEL,
        help="Maximum number of parallel agents",
    )
    parser.add_argument(
        "--max-iterations",
        type=int,
        default=None,
        help="Maximum iteration cycles",
    )
    parser.add_argument(
        "--yolo",
        action="store_true",
        help="Enable YOLO mode (skip browser testing)",
    )

    args = parser.parse_args()

    # Resolve project directory
    project_path = Path(args.project_dir)
    if not project_path.is_absolute():
        # Try to resolve from registry
        try:
            from registry import get_project_path
            resolved = get_project_path(args.project_dir)
            if resolved:
                project_path = resolved
        except ImportError:
            pass

    if not project_path.exists():
        project_path.mkdir(parents=True, exist_ok=True)

    # Run
    result = asyncio.run(run_parallel_agents(
        project_dir=project_path,
        model=args.model,
        max_parallel=args.max_parallel,
        max_iterations=args.max_iterations,
        yolo_mode=args.yolo,
    ))

    # Print summary
    print("\n" + "=" * 70)
    print("  PARALLEL RUN COMPLETE")
    print("=" * 70)
    print(f"\nTotal features processed: {result.total_features}")
    print(f"Completed: {result.completed}")
    print(f"Failed: {result.failed}")
    print(f"Duration: {result.duration_seconds:.1f}s")

    if result.failed > 0:
        print("\nFailed features:")
        for r in result.results:
            if not r.success:
                print(f"  - #{r.feature_id}: {r.feature_name}")
                print(f"    Error: {r.error}")


Now let me update the todo list to mark Phase 4.3 as complete and move on to Phase 5.


TodoWrite

Phase 1: Foundation (sandbox, credentials, resources, context injection)

Phase 2: Claude Terminal (backend + frontend)

Phase 3: Git Agent (backend + frontend)

Phase 4.1: Create feature_graph.py for dependency tracking

Phase 4.2: Create file_lock.py for concurrent access control

Phase 4.3: Create parallel_agent.py runner

Phase 5: Integration and UI updates


Phase 3: Git Agent (backend + frontend)

Phase 5: Integration and UI updates

Phase 3: Git Agent (backend + frontend)

Phase 5: Integration and UI updates


Phase 4 is complete. Now let me move on to Phase 5: Integration and UI updates. According to the plan, this includes:

1. Configuration file (`~/.autocoder/config.json`)
2. Add Terminal button/tab to App.tsx (the TerminalChat component exists but isn't accessible from the main UI)
3. Update main.py to register terminal router properly
4. Any final polish

Let me first check if the terminal router is properly registered in main.py, then add the Terminal button to App.tsx.


Phase 4 complete. Now let me add the Terminal UI button to App.tsx and create the configuration file.


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

   141→      {/* Header */}
   142→
   143→

   144→

   145→            {/* Logo and Title */}
   146→

   147→              AutoCoder
   148→

   149→
   150→            {/* Controls */}
   151→

   152→              <ProjectSelector
   153→                projects={projects ?? []}
   154→                selectedProject={selectedProject}
   155→                onSelectProject={handleSelectProject}
   156→                isLoading={projectsLoading}
   157→              />
   158→
   159→              {selectedProject && (
   160→                <>
   161→                  <button
   162→                    onClick={() => setShowAddFeature(true)}
   163→                    className="neo-btn neo-btn-primary text-sm"
   164→                    title="Press N"
   165→                  >
   166→                    <Plus size={18} />
   167→                    Add Feature
   168→
   169→                      N
   170→
   171→                  </button>
   172→
   173→                  <button
   174→                    onClick={() => setGitPanelOpen(true)}
   175→                    className="neo-btn text-sm"
   176→                    title="Press G"
   177→                  >
   178→                    <GitBranch size={18} />
   179→                    Git
   180→
   181→                      G
   182→
   183→                  </button>
   184→
   185→                  <AgentControl
   186→                    projectName={selectedProject}
   187→                    status={wsState.agentStatus}
   188→                    yoloMode={agentStatusData?.yolo_mode ?? false}
   189→                  />
   190→                </>
   191→              )}
   192→

   193→

   194→

   195→
   196→
   197→      {/* Main Content */}
   198→
   202→        {!selectedProject ? (
   203→

   204→

   205→              Welcome to AutoCoder
   206→

   207→

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

   210→

   211→        ) : (
   212→

   213→            {/* Progress Dashboard */}
   214→            <ProgressDashboard
   215→              passing={progress.passing}
   216→              total={progress.total}
   217→              percentage={progress.percentage}
   218→              isConnected={wsState.isConnected}
   219→            />
   220→
   221→            {/* Agent Thought - shows latest agent narrative */}
   222→            <AgentThought
   223→              logs={wsState.logs}
   224→              agentStatus={wsState.agentStatus}
   225→            />
   226→
   227→            {/* Initializing Features State - show when agent is running but no features yet */}
   228→            {features &&
   229→             features.pending.length === 0 &&
   230→             features.in_progress.length === 0 &&
   231→             features.done.length === 0 &&
   232→             wsState.agentStatus === 'running' && (
   233→

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

   236→                  Initializing Features...
   237→

   238→

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

   241→

   242→            )}
   243→
   244→            {/* Kanban Board */}
   245→            <KanbanBoard
   246→              features={features}
   247→              onFeatureClick={setSelectedFeature}
   248→            />
   249→

   250→        )}
   251→
   252→
   253→      {/* Add Feature Modal */}
   254→      {showAddFeature && selectedProject && (
   255→        <AddFeatureForm
   256→          projectName={selectedProject}
   257→          onClose={() => setShowAddFeature(false)}
   258→        />
   259→      )}
   260→
   261→      {/* Feature Detail Modal */}
   262→      {selectedFeature && selectedProject && (
   263→        <FeatureModal
   264→          feature={selectedFeature}
   265→          projectName={selectedProject}
   266→          onClose={() => setSelectedFeature(null)}
   267→        />
   268→      )}
   269→
   270→      {/* Debug Log Viewer - fixed to bottom */}
   271→      {selectedProject && (
   272→        <DebugLogViewer
   273→          logs={wsState.logs}
   274→          isOpen={debugOpen}
   275→          onToggle={() => setDebugOpen(!debugOpen)}
   276→          onClear={wsState.clearLogs}
   277→          onHeightChange={setDebugPanelHeight}
   278→        />
   279→      )}
   280→
   281→      {/* Assistant FAB and Panel */}
   282→      {selectedProject && (
   283→        <>
   284→          <AssistantFAB
   285→            onClick={() => setAssistantOpen(!assistantOpen)}
   286→            isOpen={assistantOpen}
   287→          />
   288→          <AssistantPanel
   289→            projectName={selectedProject}
   290→            isOpen={assistantOpen}
   291→            onClose={() => setAssistantOpen(false)}
   292→          />
   293→        </>
   294→      )}
   295→
   296→      {/* Git Panel */}
   297→      {selectedProject && (
   298→        <GitPanel
   299→          projectName={selectedProject}
   300→          isOpen={gitPanelOpen}
   301→          onClose={() => setGitPanelOpen(false)}
   302→        />
   303→      )}
   304→

   305→  )
   306→}
   307→
   308→export default App
   309→

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

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

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

                      N

                  </button>

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

                      G

                  </button>

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

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

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

        ) : (

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

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

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

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

                  Initializing Features...

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

            )}

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

        )}

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

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

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

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

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

  )
}

export default App


The App.tsx already has the Git panel and button integrated. Now I need to add the Terminal button and a TerminalPanel component (a wrapper like AssistantPanel that contains TerminalChat).Let me add:1. Import for TerminalChat
2. State for terminalOpen3. Keyboard shortcut (T key) for terminal4. Terminal button in header5. Terminal panel component


Let me add the Terminal button and panel to App.tsx.


Edit

import { GitPanel } from './components/GitPanel'
import { Plus, Loader2, GitBranch } from 'lucide-react'

import { GitPanel } from './components/GitPanel'
import { TerminalChat } from './components/TerminalChat'
import { Plus, Loader2, GitBranch, Terminal, X } from 'lucide-react'


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\App.tsx has been updated successfully.

import { GitPanel } from './components/GitPanel'
import { TerminalChat } from './components/TerminalChat'
import { Plus, Loader2, GitBranch, Terminal, X } from 'lucide-react'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

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

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

                      N

                  </button>

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

                      G

                  </button>

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

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

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

        ) : (

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

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

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

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

                  Initializing Features...

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

            )}

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

        )}

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

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

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

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

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

  )
}

export default App

-import { Plus, Loader2, GitBranch } from 'lucide-react'

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

+import { Plus, Loader2, GitBranch, Terminal, X } from 'lucide-react'


Now let me add the terminal state and keyboard shortcut.


const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)

const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)
  const [terminalOpen, setTerminalOpen] = useState(false)