The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\server\services\terminal_session.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   188→            lines.append("_No resources configured yet._")
   189→
   190→        return "\n".join(lines)
   191→
   192→    async def start(self, force_new: bool = False) -> AsyncGenerator[dict, None]:
   193→        """
   194→        Initialize session and send initial greeting, or restore existing session.
   195→
   196→        If the session was already started (reconnection), yields the message
   197→        history instead of creating a new greeting.
   198→
   199→        Args:
   200→            force_new: If True, force a new session even if one exists
   201→
   202→        Yields message chunks as they stream in.
   203→        """
   204→        # If session already started and not forcing new, restore history
   205→        if self._session_started and not force_new and self._client_entered:
   206→            # Send history restoration message
   207→            yield {"type": "session_restored", "message_count": len(self.messages)}
   208→
   209→            # Send all previous messages
   210→            for msg in self.messages:
   211→                yield {
   212→                    "type": "history",
   213→                    "role": msg.get("role", "assistant"),
   214→                    "content": msg.get("content", ""),
   215→                    "timestamp": msg.get("timestamp"),
   216→                }
   217→
   218→            # Send current resources
   219→            yield {"type": "resources", "data": self.get_resources()}
   220→            yield {"type": "response_done"}
   221→            return
   222→
   223→        # Ensure project directory exists
   224→        self.project_dir.mkdir(parents=True, exist_ok=True)
   225→
   226→        # Create security settings with full access
   227→        security_settings = {
   228→            "sandbox": {"enabled": False},  # Full system access

    async def start(self, force_new: bool = False) -> AsyncGenerator[dict, None]:
        """
        Initialize session and send initial greeting, or restore existing session.

        If the session was already started (reconnection), yields the message
        history instead of creating a new greeting.

        Args:
            force_new: If True, force a new session even if one exists

        Yields message chunks as they stream in.
        """
        # If session already started and not forcing new, restore history
        if self._session_started and not force_new and self._client_entered:
            # Send history restoration message
            yield {"type": "session_restored", "message_count": len(self.messages)}

            # Send all previous messages
            for msg in self.messages:
                yield {
                    "type": "history",
                    "role": msg.get("role", "assistant"),
                    "content": msg.get("content", ""),
                    "timestamp": msg.get("timestamp"),
                }

            # Send current resources
            yield {"type": "resources", "data": self.get_resources()}
            yield {"type": "response_done"}
            return

        # Ensure project directory exists
        self.project_dir.mkdir(parents=True, exist_ok=True)

"""
Terminal Chat Session
=====================

Manages interactive Claude terminal sessions for resource management,
infrastructure discovery, and nuanced configuration tasks.

The terminal provides full system access (no sandbox) and allows users to:
- Add and configure servers, services, and credentials
- Test connectivity to resources
- Save resources as project-specific or global
- Execute system commands for infrastructure tasks
"""

import json
import logging
import shutil
import threading
from datetime import datetime
from pathlib import Path
from typing import AsyncGenerator, Optional

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient

from .credential_resolver import get_resolver
from .resource_manager import (
    get_manager,
    Resource,
    ResourceType,
    ResourceScope,
    AuthType,
    ResourceAuth,
)

logger = logging.getLogger(__name__)

# Root directory of the project
ROOT_DIR = Path(__file__).parent.parent.parent

# Terminal system prompt
TERMINAL_SYSTEM_PROMPT = """You are a resource management assistant for Autocoder 2.0.

## Your Role

You help users manage infrastructure resources (servers, services, databases) that coding agents can use. Your job is to:

1. **Add Resources** - When users mention a server or service, help add it to their resources
2. **Test Connectivity** - Verify resources are accessible before saving
3. **Probe Capabilities** - Discover what tools/runtimes are installed on servers
4. **Manage Scope** - Help users decide if resources should be project-specific or global

## Available Tools

You have full system access:
- **Bash**: SSH, curl, git, docker, and any system command
- **Read/Write/Edit**: File operations
- **Glob/Grep**: Search files

## When Adding a Resource

Follow this workflow:
1. Get connection details from user (host, user, credentials)
2. Test connectivity (SSH for servers, curl for services)
3. Probe for relevant capabilities if it's a server
4. Ask user: "Should this be a global resource (all projects) or project-specific?"
5. Save to the appropriate resources.json

## Resource Storage

Resources are stored in JSON files:
- **Global**: `~/.autocoder/global_resources.json`
- **Project**: `{project_dir}/resources.json`

Use the resource management functions to save resources programmatically.

## Credential Handling

Credentials are stored separately in `credentials.md`. When saving a resource:
- Don't store passwords directly in resources.json
- Instead, use a `credential_ref` that points to credentials.md
- Example: `{"auth": {"type": "ssh-password", "credential_ref": "jupiter"}}`

## Example Interactions

User: "Add my build server at 172.16.3.30"
You: Test SSH, probe for rust/node/docker, ask about scope, save resource

User: "What servers do I have configured?"
You: List resources from global + project resources

User: "Test if the Gitea API is working"
You: curl the API endpoint, report results

## Project Context

{PROJECT_CONTEXT}

## Current Working Directory

{CWD}
"""

class TerminalSession:
    """
    Manages an interactive Claude terminal session for one project.

    The terminal provides full system access for:
    - Resource management (add servers, services)
    - Connection testing and capability probing
    - Infrastructure configuration

    Sessions are persistent - they survive UI disconnects and can be resumed.
    """

    def __init__(
        self,
        project_name: str,
        project_dir: Path,
    ):
        """
        Initialize the terminal session.

        Args:
            project_name: Name of the project
            project_dir: Absolute path to the project directory
        """
        self.project_name = project_name
        self.project_dir = project_dir
        self.client: Optional[ClaudeSDKClient] = None
        self.messages: list[dict] = []
        self.created_at = datetime.now()
        self._client_entered: bool = False
        self._session_started: bool = False  # Track if initial greeting was sent
        self._resource_manager = get_manager()
        self._credential_resolver = get_resolver()

    def get_resources(self) -> dict:
        """Get current resources for the project."""
        resources = self._resource_manager.get_merged_resources(
            self.project_name, self.project_dir
        )
        return {
            name: resource.to_dict()
            for name, resource in resources.items()
        }

    async def close(self) -> None:
        """Clean up resources and close the Claude client."""
        if self.client and self._client_entered:
            try:
                await self.client.__aexit__(None, None, None)
            except Exception as e:
                logger.warning(f"Error closing Claude client: {e}")
            finally:
                self._client_entered = False
                self.client = None

    def get_message_history(self) -> list[dict]:
        """
        Get the message history for this session.

        Returns:
            List of message dicts with role, content, timestamp
        """
        return self.messages.copy()

    def is_active(self) -> bool:
        """Check if session has been started and is active."""
        return self._session_started and self._client_entered

    def _get_project_context(self) -> str:
        """Generate project context for the system prompt."""
        lines = [f"Project: **{self.project_name}**"]
        lines.append(f"Directory: `{self.project_dir}`")
        lines.append("")

        # Get current resources
        resources = self.get_resources()
        if resources:
            lines.append("### Current Resources")
            for name, data in resources.items():
                rtype = data.get("type", "unknown")
                scope = data.get("scope", "project")
                host = data.get("host") or data.get("url") or "no host"
                lines.append(f"- **{name}** ({rtype}, {scope}): {host}")
        else:
            lines.append("_No resources configured yet._")

        return "\n".join(lines)

    async def start(self) -> AsyncGenerator[dict, None]:
        """
        Initialize session and send initial greeting.

        Yields message chunks as they stream in.
        """
        # Ensure project directory exists
        self.project_dir.mkdir(parents=True, exist_ok=True)

        # Create security settings with full access
        security_settings = {
            "sandbox": {"enabled": False},  # Full system access
            "permissions": {
                "defaultMode": "acceptEdits",
                "allow": [
                    "Read(./**)",
                    "Read(~/**)",  # Allow reading home directory
                    "Write(./**)",
                    "Edit(./**)",
                    "Glob(./**)",
                    "Grep(./**)",
                    "Bash(*)",  # Full bash access
                    "WebFetch",
                    "WebSearch",
                ],
            },
        }
        settings_file = self.project_dir / ".claude_terminal_settings.json"
        with open(settings_file, "w") as f:
            json.dump(security_settings, f, indent=2)

        # Build system prompt with context
        system_prompt = TERMINAL_SYSTEM_PROMPT.replace(
            "{PROJECT_CONTEXT}",
            self._get_project_context()
        ).replace(
            "{CWD}",
            str(self.project_dir.resolve())
        )

        # Use system Claude CLI
        system_cli = shutil.which("claude")

        try:
            self.client = ClaudeSDKClient(
                options=ClaudeAgentOptions(
                    model="claude-sonnet-4-20250514",  # Sonnet for faster terminal responses
                    cli_path=system_cli,
                    system_prompt=system_prompt,
                    allowed_tools=[
                        "Read",
                        "Write",
                        "Edit",
                        "Glob",
                        "Grep",
                        "Bash",
                        "WebFetch",
                        "WebSearch",
                    ],
                    permission_mode="acceptEdits",
                    max_turns=100,
                    cwd=str(self.project_dir.resolve()),
                    settings=str(settings_file.resolve()),
                )
            )
            await self.client.__aenter__()
            self._client_entered = True
        except Exception as e:
            logger.exception("Failed to create Claude client for terminal")
            yield {
                "type": "error",
                "content": f"Failed to initialize terminal: {str(e)}"
            }
            return

        # Send initial greeting
        initial_message = (
            f"Terminal session started for project **{self.project_name}**.\n\n"
            "I can help you manage resources (servers, services, databases). "
            "What would you like to do?"
        )

        yield {"type": "text", "content": initial_message}
        yield {"type": "resources", "data": self.get_resources()}
        yield {"type": "response_done"}

    async def send_message(
        self,
        user_message: str,
    ) -> AsyncGenerator[dict, None]:
        """
        Send user message and stream Claude's response.

        Args:
            user_message: The user's message

        Yields:
            Message chunks of various types:
            - {"type": "text", "content": str}
            - {"type": "resource_added", "resource": dict}
            - {"type": "verification_result", "result": dict}
            - {"type": "error", "content": str}
        """
        if not self.client:
            yield {
                "type": "error",
                "content": "Session not initialized. Call start() first."
            }
            return

        # Store the user message
        self.messages.append({
            "role": "user",
            "content": user_message,
            "timestamp": datetime.now().isoformat()
        })

        try:
            async for chunk in self._query_claude(user_message):
                yield chunk
            yield {"type": "response_done"}
        except Exception as e:
            logger.exception("Error during terminal query")
            yield {
                "type": "error",
                "content": f"Error: {str(e)}"
            }

    async def _query_claude(
        self,
        message: str,
    ) -> AsyncGenerator[dict, None]:
        """
        Query Claude and stream responses.

        Handles tool calls and text responses.
        Detects resource management operations and yields appropriate events.
        """
        if not self.client:
            return

        await self.client.query(message)

        current_text = ""

        # Stream the response
        async for msg in self.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"):
                        text = block.text
                        if text:
                            current_text += text
                            yield {"type": "text", "content": text}

                            # Store in message history
                            self.messages.append({
                                "role": "assistant",
                                "content": text,
                                "timestamp": datetime.now().isoformat()
                            })

                    elif block_type == "ToolUseBlock":
                        tool_name = getattr(block, "name", "")
                        tool_input = getattr(block, "input", {})

                        # Detect resource file writes
                        if tool_name == "Write":
                            file_path = tool_input.get("file_path", "")
                            if "resources.json" in file_path:
                                yield {
                                    "type": "resource_updated",
                                    "path": file_path
                                }
                                # Refresh resources
                                yield {
                                    "type": "resources",
                                    "data": self.get_resources()
                                }

                        # Notify about bash commands (for transparency)
                        if tool_name == "Bash":
                            command = tool_input.get("command", "")
                            yield {
                                "type": "command_executing",
                                "command": command[:100] + ("..." if len(command) > 100 else "")
                            }

            elif msg_type == "UserMessage" and hasattr(msg, "content"):
                # Tool results - look for verification results
                for block in msg.content:
                    block_type = type(block).__name__
                    if block_type == "ToolResultBlock":
                        result = getattr(block, "result", "")
                        # Check for SSH or connection test results
                        if "connected" in str(result).lower() or "success" in str(result).lower():
                            yield {
                                "type": "connection_verified",
                                "result": str(result)[:200]
                            }

# Session registry - thread-safe storage of active sessions
_sessions: dict[str, TerminalSession] = {}
_sessions_lock = threading.Lock()

async def get_or_create_terminal_session(
    project_name: str,
    project_dir: Path,
) -> TerminalSession:
    """
    Get existing terminal session or create a new one.

    Args:
        project_name: Name of the project
        project_dir: Path to the project directory

    Returns:
        TerminalSession instance
    """
    with _sessions_lock:
        # Check for existing session
        if project_name in _sessions:
            session = _sessions[project_name]
            # Verify it's still valid
            if session.client and session._client_entered:
                return session
            # Invalid session - remove it
            del _sessions[project_name]

        # Create new session
        session = TerminalSession(project_name, project_dir)
        _sessions[project_name] = session

    return session

async def close_terminal_session(project_name: str) -> None:
    """
    Close and remove a terminal session.

    Args:
        project_name: Name of the project
    """
    session = None
    with _sessions_lock:
        session = _sessions.pop(project_name, None)

    if session:
        await session.close()

def get_active_sessions() -> list[str]:
    """Get list of project names with active terminal sessions."""
    with _sessions_lock:
        return list(_sessions.keys())

-    async def start(self) -> AsyncGenerator[dict, None]:

+    async def start(self, force_new: bool = False) -> AsyncGenerator[dict, None]:

-        Initialize session and send initial greeting.

+        Initialize session and send initial greeting, or restore existing session.

+        If the session was already started (reconnection), yields the message

+        history instead of creating a new greeting.

+            force_new: If True, force a new session even if one exists

+        # If session already started and not forcing new, restore history

+        if self._session_started and not force_new and self._client_entered:

+            # Send history restoration message

+            yield {"type": "session_restored", "message_count": len(self.messages)}

+            # Send all previous messages

+                    "role": msg.get("role", "assistant"),

+                    "content": msg.get("content", ""),

+                    "timestamp": msg.get("timestamp"),

+            yield {"type": "resources", "data": self.get_resources()}

+            yield {"type": "response_done"}


Good, now I need to add the flag to mark the session as started after the initial greeting is sent. Let me find that section and add the flag.


Edit

        # Send initial greeting
        initial_message = (
            f"Terminal session started for project **{self.project_name}**.\n\n"
            "I can help you manage resources (servers, services, databases). "
            "What would you like to do?"
        )

        yield {"type": "text", "content": initial_message}
        yield {"type": "resources", "data": self.get_resources()}
        yield {"type": "response_done"}

        # Mark session as started
        self._session_started = True

        # Send initial greeting
        initial_message = (
            f"Terminal session started for project **{self.project_name}**.\n\n"
            "I can help you manage resources (servers, services, databases). "
            "What would you like to do?"
        )

        # Store the greeting in message history
        self.messages.append({
            "role": "assistant",
            "content": initial_message,
            "timestamp": datetime.now().isoformat()
        })

        yield {"type": "text", "content": initial_message}
        yield {"type": "resources", "data": self.get_resources()}
        yield {"type": "response_done"}


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\server\services\terminal_session.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   288→                "content": f"Failed to initialize terminal: {str(e)}"
   289→            }
   290→            return
   291→
   292→        # Mark session as started
   293→        self._session_started = True
   294→
   295→        # Send initial greeting
   296→        initial_message = (
   297→            f"Terminal session started for project **{self.project_name}**.\n\n"
   298→            "I can help you manage resources (servers, services, databases). "
   299→            "What would you like to do?"
   300→        )
   301→
   302→        # Store the greeting in message history
   303→        self.messages.append({
   304→            "role": "assistant",
   305→            "content": initial_message,
   306→            "timestamp": datetime.now().isoformat()
   307→        })
   308→
   309→        yield {"type": "text", "content": initial_message}
   310→        yield {"type": "resources", "data": self.get_resources()}
   311→        yield {"type": "response_done"}
   312→
   313→    async def send_message(
   314→        self,
   315→        user_message: str,

        # Mark session as started
        self._session_started = True

        # Send initial greeting
        initial_message = (
            f"Terminal session started for project **{self.project_name}**.\n\n"
            "I can help you manage resources (servers, services, databases). "
            "What would you like to do?"
        )

        # Store the greeting in message history
        self.messages.append({
            "role": "assistant",
            "content": initial_message,
            "timestamp": datetime.now().isoformat()
        })

        yield {"type": "text", "content": initial_message}
        yield {"type": "resources", "data": self.get_resources()}
        yield {"type": "response_done"}

"""
Terminal Chat Session
=====================

Manages interactive Claude terminal sessions for resource management,
infrastructure discovery, and nuanced configuration tasks.

The terminal provides full system access (no sandbox) and allows users to:
- Add and configure servers, services, and credentials
- Test connectivity to resources
- Save resources as project-specific or global
- Execute system commands for infrastructure tasks
"""

import json
import logging
import shutil
import threading
from datetime import datetime
from pathlib import Path
from typing import AsyncGenerator, Optional

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient

from .credential_resolver import get_resolver
from .resource_manager import (
    get_manager,
    Resource,
    ResourceType,
    ResourceScope,
    AuthType,
    ResourceAuth,
)

logger = logging.getLogger(__name__)

# Root directory of the project
ROOT_DIR = Path(__file__).parent.parent.parent

# Terminal system prompt
TERMINAL_SYSTEM_PROMPT = """You are a resource management assistant for Autocoder 2.0.

## Your Role

You help users manage infrastructure resources (servers, services, databases) that coding agents can use. Your job is to:

1. **Add Resources** - When users mention a server or service, help add it to their resources
2. **Test Connectivity** - Verify resources are accessible before saving
3. **Probe Capabilities** - Discover what tools/runtimes are installed on servers
4. **Manage Scope** - Help users decide if resources should be project-specific or global

## Available Tools

You have full system access:
- **Bash**: SSH, curl, git, docker, and any system command
- **Read/Write/Edit**: File operations
- **Glob/Grep**: Search files

## When Adding a Resource

Follow this workflow:
1. Get connection details from user (host, user, credentials)
2. Test connectivity (SSH for servers, curl for services)
3. Probe for relevant capabilities if it's a server
4. Ask user: "Should this be a global resource (all projects) or project-specific?"
5. Save to the appropriate resources.json

## Resource Storage

Resources are stored in JSON files:
- **Global**: `~/.autocoder/global_resources.json`
- **Project**: `{project_dir}/resources.json`

Use the resource management functions to save resources programmatically.

## Credential Handling

Credentials are stored separately in `credentials.md`. When saving a resource:
- Don't store passwords directly in resources.json
- Instead, use a `credential_ref` that points to credentials.md
- Example: `{"auth": {"type": "ssh-password", "credential_ref": "jupiter"}}`

## Example Interactions

User: "Add my build server at 172.16.3.30"
You: Test SSH, probe for rust/node/docker, ask about scope, save resource

User: "What servers do I have configured?"
You: List resources from global + project resources

User: "Test if the Gitea API is working"
You: curl the API endpoint, report results

## Project Context

{PROJECT_CONTEXT}

## Current Working Directory

{CWD}
"""

class TerminalSession:
    """
    Manages an interactive Claude terminal session for one project.

    The terminal provides full system access for:
    - Resource management (add servers, services)
    - Connection testing and capability probing
    - Infrastructure configuration

    Sessions are persistent - they survive UI disconnects and can be resumed.
    """

    def __init__(
        self,
        project_name: str,
        project_dir: Path,
    ):
        """
        Initialize the terminal session.

        Args:
            project_name: Name of the project
            project_dir: Absolute path to the project directory
        """
        self.project_name = project_name
        self.project_dir = project_dir
        self.client: Optional[ClaudeSDKClient] = None
        self.messages: list[dict] = []
        self.created_at = datetime.now()
        self._client_entered: bool = False
        self._session_started: bool = False  # Track if initial greeting was sent
        self._resource_manager = get_manager()
        self._credential_resolver = get_resolver()

    def get_resources(self) -> dict:
        """Get current resources for the project."""
        resources = self._resource_manager.get_merged_resources(
            self.project_name, self.project_dir
        )
        return {
            name: resource.to_dict()
            for name, resource in resources.items()
        }

    async def close(self) -> None:
        """Clean up resources and close the Claude client."""
        if self.client and self._client_entered:
            try:
                await self.client.__aexit__(None, None, None)
            except Exception as e:
                logger.warning(f"Error closing Claude client: {e}")
            finally:
                self._client_entered = False
                self.client = None

    def get_message_history(self) -> list[dict]:
        """
        Get the message history for this session.

        Returns:
            List of message dicts with role, content, timestamp
        """
        return self.messages.copy()

    def is_active(self) -> bool:
        """Check if session has been started and is active."""
        return self._session_started and self._client_entered

    def _get_project_context(self) -> str:
        """Generate project context for the system prompt."""
        lines = [f"Project: **{self.project_name}**"]
        lines.append(f"Directory: `{self.project_dir}`")
        lines.append("")

        # Get current resources
        resources = self.get_resources()
        if resources:
            lines.append("### Current Resources")
            for name, data in resources.items():
                rtype = data.get("type", "unknown")
                scope = data.get("scope", "project")
                host = data.get("host") or data.get("url") or "no host"
                lines.append(f"- **{name}** ({rtype}, {scope}): {host}")
        else:
            lines.append("_No resources configured yet._")

        return "\n".join(lines)

    async def start(self, force_new: bool = False) -> AsyncGenerator[dict, None]:
        """
        Initialize session and send initial greeting, or restore existing session.

        If the session was already started (reconnection), yields the message
        history instead of creating a new greeting.

        Args:
            force_new: If True, force a new session even if one exists

        Yields message chunks as they stream in.
        """
        # If session already started and not forcing new, restore history
        if self._session_started and not force_new and self._client_entered:
            # Send history restoration message
            yield {"type": "session_restored", "message_count": len(self.messages)}

            # Send all previous messages
            for msg in self.messages:
                yield {
                    "type": "history",
                    "role": msg.get("role", "assistant"),
                    "content": msg.get("content", ""),
                    "timestamp": msg.get("timestamp"),
                }

            # Send current resources
            yield {"type": "resources", "data": self.get_resources()}
            yield {"type": "response_done"}
            return

        # Ensure project directory exists
        self.project_dir.mkdir(parents=True, exist_ok=True)

        # Create security settings with full access
        security_settings = {
            "sandbox": {"enabled": False},  # Full system access
            "permissions": {
                "defaultMode": "acceptEdits",
                "allow": [
                    "Read(./**)",
                    "Read(~/**)",  # Allow reading home directory
                    "Write(./**)",
                    "Edit(./**)",
                    "Glob(./**)",
                    "Grep(./**)",
                    "Bash(*)",  # Full bash access
                    "WebFetch",
                    "WebSearch",
                ],
            },
        }
        settings_file = self.project_dir / ".claude_terminal_settings.json"
        with open(settings_file, "w") as f:
            json.dump(security_settings, f, indent=2)

        # Build system prompt with context
        system_prompt = TERMINAL_SYSTEM_PROMPT.replace(
            "{PROJECT_CONTEXT}",
            self._get_project_context()
        ).replace(
            "{CWD}",
            str(self.project_dir.resolve())
        )

        # Use system Claude CLI
        system_cli = shutil.which("claude")

        try:
            self.client = ClaudeSDKClient(
                options=ClaudeAgentOptions(
                    model="claude-sonnet-4-20250514",  # Sonnet for faster terminal responses
                    cli_path=system_cli,
                    system_prompt=system_prompt,
                    allowed_tools=[
                        "Read",
                        "Write",
                        "Edit",
                        "Glob",
                        "Grep",
                        "Bash",
                        "WebFetch",
                        "WebSearch",
                    ],
                    permission_mode="acceptEdits",
                    max_turns=100,
                    cwd=str(self.project_dir.resolve()),
                    settings=str(settings_file.resolve()),
                )
            )
            await self.client.__aenter__()
            self._client_entered = True
        except Exception as e:
            logger.exception("Failed to create Claude client for terminal")
            yield {
                "type": "error",
                "content": f"Failed to initialize terminal: {str(e)}"
            }
            return

        # Send initial greeting
        initial_message = (
            f"Terminal session started for project **{self.project_name}**.\n\n"
            "I can help you manage resources (servers, services, databases). "
            "What would you like to do?"
        )

        yield {"type": "text", "content": initial_message}
        yield {"type": "resources", "data": self.get_resources()}
        yield {"type": "response_done"}

    async def send_message(
        self,
        user_message: str,
    ) -> AsyncGenerator[dict, None]:
        """
        Send user message and stream Claude's response.

        Args:
            user_message: The user's message

        Yields:
            Message chunks of various types:
            - {"type": "text", "content": str}
            - {"type": "resource_added", "resource": dict}
            - {"type": "verification_result", "result": dict}
            - {"type": "error", "content": str}
        """
        if not self.client:
            yield {
                "type": "error",
                "content": "Session not initialized. Call start() first."
            }
            return

        # Store the user message
        self.messages.append({
            "role": "user",
            "content": user_message,
            "timestamp": datetime.now().isoformat()
        })

        try:
            async for chunk in self._query_claude(user_message):
                yield chunk
            yield {"type": "response_done"}
        except Exception as e:
            logger.exception("Error during terminal query")
            yield {
                "type": "error",
                "content": f"Error: {str(e)}"
            }

    async def _query_claude(
        self,
        message: str,
    ) -> AsyncGenerator[dict, None]:
        """
        Query Claude and stream responses.

        Handles tool calls and text responses.
        Detects resource management operations and yields appropriate events.
        """
        if not self.client:
            return

        await self.client.query(message)

        current_text = ""

        # Stream the response
        async for msg in self.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"):
                        text = block.text
                        if text:
                            current_text += text
                            yield {"type": "text", "content": text}

                            # Store in message history
                            self.messages.append({
                                "role": "assistant",
                                "content": text,
                                "timestamp": datetime.now().isoformat()
                            })

                    elif block_type == "ToolUseBlock":
                        tool_name = getattr(block, "name", "")
                        tool_input = getattr(block, "input", {})

                        # Detect resource file writes
                        if tool_name == "Write":
                            file_path = tool_input.get("file_path", "")
                            if "resources.json" in file_path:
                                yield {
                                    "type": "resource_updated",
                                    "path": file_path
                                }
                                # Refresh resources
                                yield {
                                    "type": "resources",
                                    "data": self.get_resources()
                                }

                        # Notify about bash commands (for transparency)
                        if tool_name == "Bash":
                            command = tool_input.get("command", "")
                            yield {
                                "type": "command_executing",
                                "command": command[:100] + ("..." if len(command) > 100 else "")
                            }

            elif msg_type == "UserMessage" and hasattr(msg, "content"):
                # Tool results - look for verification results
                for block in msg.content:
                    block_type = type(block).__name__
                    if block_type == "ToolResultBlock":
                        result = getattr(block, "result", "")
                        # Check for SSH or connection test results
                        if "connected" in str(result).lower() or "success" in str(result).lower():
                            yield {
                                "type": "connection_verified",
                                "result": str(result)[:200]
                            }

# Session registry - thread-safe storage of active sessions
_sessions: dict[str, TerminalSession] = {}
_sessions_lock = threading.Lock()

async def get_or_create_terminal_session(
    project_name: str,
    project_dir: Path,
) -> TerminalSession:
    """
    Get existing terminal session or create a new one.

    Args:
        project_name: Name of the project
        project_dir: Path to the project directory

    Returns:
        TerminalSession instance
    """
    with _sessions_lock:
        # Check for existing session
        if project_name in _sessions:
            session = _sessions[project_name]
            # Verify it's still valid
            if session.client and session._client_entered:
                return session
            # Invalid session - remove it
            del _sessions[project_name]

        # Create new session
        session = TerminalSession(project_name, project_dir)
        _sessions[project_name] = session

    return session

async def close_terminal_session(project_name: str) -> None:
    """
    Close and remove a terminal session.

    Args:
        project_name: Name of the project
    """
    session = None
    with _sessions_lock:
        session = _sessions.pop(project_name, None)

    if session:
        await session.close()

def get_active_sessions() -> list[str]:
    """Get list of project names with active terminal sessions."""
    with _sessions_lock:
        return list(_sessions.keys())

+        # Store the greeting in message history

+            "content": initial_message,

+            "timestamp": datetime.now().isoformat()


Now I need to update the WebSocket handler in terminal.py to handle the new message types and pass the force_new parameter. Let me also add a "new_session" message type to allow the UI to force a fresh start.


@router.websocket("/ws/{project_name}")
async def terminal_websocket(websocket: WebSocket, project_name: str):
    """
    WebSocket endpoint for interactive terminal chat.

    Message protocol:

    Client -> Server:
    - {"type": "start"} - Start the terminal session
    - {"type": "message", "content": "..."} - Send user message
    - {"type": "ping"} - Keep-alive ping

    Server -> Client:
    - {"type": "text", "content": "..."} - Text chunk from Claude
    - {"type": "resources", "data": {...}} - Resource list update
    - {"type": "resource_updated", "path": "..."} - Resource file updated
    - {"type": "command_executing", "command": "..."} - Bash command being run
    - {"type": "connection_verified", "result": "..."} - Connection test result
    - {"type": "response_done"} - Response complete
    - {"type": "error", "content": "..."} - Error message
    - {"type": "pong"} - Keep-alive pong
    """

@router.websocket("/ws/{project_name}")
async def terminal_websocket(websocket: WebSocket, project_name: str):
    """
    WebSocket endpoint for interactive terminal chat.

    Sessions are PERSISTENT - they survive UI disconnects and can be resumed.
    When reconnecting, the full message history is restored.

    Message protocol:

    Client -> Server:
    - {"type": "start"} - Start or resume terminal session (restores history if exists)
    - {"type": "new_session"} - Force a new session, discarding history
    - {"type": "message", "content": "..."} - Send user message
    - {"type": "get_history"} - Request message history without restarting
    - {"type": "ping"} - Keep-alive ping

    Server -> Client:
    - {"type": "text", "content": "..."} - Text chunk from Claude
    - {"type": "history", "role": "...", "content": "..."} - Historical message (on reconnect)
    - {"type": "session_restored", "message_count": N} - Session was restored with N messages
    - {"type": "resources", "data": {...}} - Resource list update
    - {"type": "resource_updated", "path": "..."} - Resource file updated
    - {"type": "command_executing", "command": "..."} - Bash command being run
    - {"type": "connection_verified", "result": "..."} - Connection test result
    - {"type": "response_done"} - Response complete
    - {"type": "error", "content": "..."} - Error message
    - {"type": "pong"} - Keep-alive pong
    """


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\server\routers\terminal.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   283→# ============================================================================
   284→# WebSocket Endpoint
   285→# ============================================================================
   286→
   287→@router.websocket("/ws/{project_name}")
   288→async def terminal_websocket(websocket: WebSocket, project_name: str):
   289→    """
   290→    WebSocket endpoint for interactive terminal chat.
   291→
   292→    Sessions are PERSISTENT - they survive UI disconnects and can be resumed.
   293→    When reconnecting, the full message history is restored.
   294→
   295→    Message protocol:
   296→
   297→    Client -> Server:
   298→    - {"type": "start"} - Start or resume terminal session (restores history if exists)
   299→    - {"type": "new_session"} - Force a new session, discarding history
   300→    - {"type": "message", "content": "..."} - Send user message
   301→    - {"type": "get_history"} - Request message history without restarting
   302→    - {"type": "ping"} - Keep-alive ping
   303→
   304→    Server -> Client:
   305→    - {"type": "text", "content": "..."} - Text chunk from Claude
   306→    - {"type": "history", "role": "...", "content": "..."} - Historical message (on reconnect)
   307→    - {"type": "session_restored", "message_count": N} - Session was restored with N messages
   308→    - {"type": "resources", "data": {...}} - Resource list update
   309→    - {"type": "resource_updated", "path": "..."} - Resource file updated
   310→    - {"type": "command_executing", "command": "..."} - Bash command being run
   311→    - {"type": "connection_verified", "result": "..."} - Connection test result
   312→    - {"type": "response_done"} - Response complete
   313→    - {"type": "error", "content": "..."} - Error message
   314→    - {"type": "pong"} - Keep-alive pong
   315→    """
   316→    if not validate_project_name(project_name):
   317→        await websocket.close(code=4000, reason="Invalid project name")
   318→        return
   319→

@router.websocket("/ws/{project_name}")
async def terminal_websocket(websocket: WebSocket, project_name: str):
    """
    WebSocket endpoint for interactive terminal chat.

    Sessions are PERSISTENT - they survive UI disconnects and can be resumed.
    When reconnecting, the full message history is restored.

    Message protocol:

    Client -> Server:
    - {"type": "start"} - Start or resume terminal session (restores history if exists)
    - {"type": "new_session"} - Force a new session, discarding history
    - {"type": "message", "content": "..."} - Send user message
    - {"type": "get_history"} - Request message history without restarting
    - {"type": "ping"} - Keep-alive ping

    Server -> Client:
    - {"type": "text", "content": "..."} - Text chunk from Claude
    - {"type": "history", "role": "...", "content": "..."} - Historical message (on reconnect)
    - {"type": "session_restored", "message_count": N} - Session was restored with N messages
    - {"type": "resources", "data": {...}} - Resource list update
    - {"type": "resource_updated", "path": "..."} - Resource file updated
    - {"type": "command_executing", "command": "..."} - Bash command being run
    - {"type": "connection_verified", "result": "..."} - Connection test result
    - {"type": "response_done"} - Response complete
    - {"type": "error", "content": "..."} - Error message
    - {"type": "pong"} - Keep-alive pong
    """

"""
Terminal Router
===============

WebSocket and REST endpoints for interactive Claude terminal sessions.
The terminal provides full system access for resource management and
infrastructure configuration.
"""

import json
import logging
import re
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
from pydantic import BaseModel

from ..services.terminal_session import (
    TerminalSession,
    get_or_create_terminal_session,
    close_terminal_session,
    get_active_sessions,
)
from ..services.resource_manager import (
    get_manager,
    Resource,
    ResourceType,
    ResourceScope,
)

logger = logging.getLogger(__name__)

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

# Root directory
ROOT_DIR = Path(__file__).parent.parent.parent

def _get_project_path(project_name: str) -> Path:
    """Get project path from registry."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import get_project_path
    return get_project_path(project_name)

def validate_project_name(name: str) -> bool:
    """Validate project name to prevent path traversal."""
    return bool(re.match(r'^[a-zA-Z0-9_-]{1,50}$', name))

# ============================================================================
# REST Endpoints
# ============================================================================

class TerminalSessionStatus(BaseModel):
    """Status of a terminal session."""
    project_name: str
    is_active: bool
    message_count: int
    resource_count: int

class ResourceInfo(BaseModel):
    """Resource information."""
    name: str
    type: str
    scope: str
    host: Optional[str] = None
    url: Optional[str] = None
    verified: bool = False

@router.get("/sessions", response_model=list[str])
async def list_terminal_sessions():
    """List all active terminal sessions."""
    return get_active_sessions()

@router.get("/sessions/{project_name}", response_model=TerminalSessionStatus)
async def get_terminal_status(project_name: str):
    """Get status of a terminal session."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    active = get_active_sessions()
    if project_name not in active:
        raise HTTPException(status_code=404, detail="No active terminal session")

    # Get resource count
    project_dir = _get_project_path(project_name)
    manager = get_manager()
    resources = manager.get_merged_resources(project_name, project_dir)

    return TerminalSessionStatus(
        project_name=project_name,
        is_active=True,
        message_count=0,  # Would need to track this in session
        resource_count=len(resources),
    )

@router.delete("/sessions/{project_name}")
async def close_session(project_name: str):
    """Close and remove a terminal session."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    await close_terminal_session(project_name)
    return {"success": True, "message": "Session closed"}

@router.get("/resources/{project_name}", response_model=list[ResourceInfo])
async def list_resources(project_name: str):
    """List resources for a project (merged global + project)."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    manager = get_manager()
    resources = manager.get_merged_resources(project_name, project_dir)

    return [
        ResourceInfo(
            name=r.name,
            type=r.type.value,
            scope=r.scope.value,
            host=r.host,
            url=r.url,
            verified=r.verified_at is not None,
        )
        for r in resources.values()
    ]

@router.get("/resources/global", response_model=list[ResourceInfo])
async def list_global_resources():
    """List all global resources."""
    manager = get_manager()
    manager._ensure_global_loaded()

    return [
        ResourceInfo(
            name=r.name,
            type=r.type.value,
            scope=r.scope.value,
            host=r.host,
            url=r.url,
            verified=r.verified_at is not None,
        )
        for r in manager._global_resources.values()
    ]

class AddResourceRequest(BaseModel):
    """Request to add a resource."""
    name: str
    type: str  # server, service, database, repository
    scope: str  # global, project
    host: Optional[str] = None
    port: Optional[int] = None
    url: Optional[str] = None
    user: Optional[str] = None
    credential_ref: Optional[str] = None
    description: Optional[str] = None

@router.post("/resources/{project_name}")
async def add_resource(project_name: str, request: AddResourceRequest):
    """Add a resource to a project or globally."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    try:
        resource_type = ResourceType(request.type)
    except ValueError:
        raise HTTPException(status_code=400, detail=f"Invalid resource type: {request.type}")

    try:
        scope = ResourceScope(request.scope)
    except ValueError:
        raise HTTPException(status_code=400, detail=f"Invalid scope: {request.scope}")

    from ..services.resource_manager import ResourceAuth, AuthType

    # Determine auth type based on credential_ref
    auth = ResourceAuth()
    if request.credential_ref:
        # Determine auth type based on resource type
        if resource_type == ResourceType.SERVER:
            auth = ResourceAuth(type=AuthType.SSH_PASSWORD, credential_ref=request.credential_ref)
        elif resource_type == ResourceType.SERVICE:
            auth = ResourceAuth(type=AuthType.API_TOKEN, credential_ref=request.credential_ref)

    resource = Resource(
        name=request.name,
        type=resource_type,
        scope=scope,
        host=request.host,
        port=request.port,
        url=request.url,
        user=request.user,
        auth=auth,
        description=request.description,
    )

    manager = get_manager()
    manager.add_resource(
        resource,
        project_name=project_name if scope == ResourceScope.PROJECT else None,
        project_dir=project_dir if scope == ResourceScope.PROJECT else None,
    )

    return {"success": True, "resource": resource.to_dict()}

@router.delete("/resources/{project_name}/{resource_name}")
async def remove_resource(project_name: str, resource_name: str, scope: str = "project"):
    """Remove a resource."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    try:
        resource_scope = ResourceScope(scope)
    except ValueError:
        raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}")

    manager = get_manager()
    removed = manager.remove_resource(
        resource_name,
        resource_scope,
        project_name=project_name if resource_scope == ResourceScope.PROJECT else None,
        project_dir=project_dir if resource_scope == ResourceScope.PROJECT else None,
    )

    if not removed:
        raise HTTPException(status_code=404, detail="Resource not found")

    return {"success": True, "message": f"Resource '{resource_name}' removed"}

@router.post("/resources/{project_name}/{resource_name}/verify")
async def verify_resource(project_name: str, resource_name: str):
    """Verify a resource is accessible."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    manager = get_manager()
    resource = manager.get_resource(resource_name, project_name, project_dir)

    if not resource:
        raise HTTPException(status_code=404, detail="Resource not found")

    result = await manager.verify_resource(resource)

    return {
        "success": result.success,
        "message": result.message,
        "capabilities": result.capabilities,
        "response_time_ms": result.response_time_ms,
    }

# ============================================================================
# WebSocket Endpoint
# ============================================================================

@router.websocket("/ws/{project_name}")
async def terminal_websocket(websocket: WebSocket, project_name: str):
    """
    WebSocket endpoint for interactive terminal chat.

    Message protocol:

    Client -> Server:
    - {"type": "start"} - Start the terminal session
    - {"type": "message", "content": "..."} - Send user message
    - {"type": "ping"} - Keep-alive ping

    Server -> Client:
    - {"type": "text", "content": "..."} - Text chunk from Claude
    - {"type": "resources", "data": {...}} - Resource list update
    - {"type": "resource_updated", "path": "..."} - Resource file updated
    - {"type": "command_executing", "command": "..."} - Bash command being run
    - {"type": "connection_verified", "result": "..."} - Connection test result
    - {"type": "response_done"} - Response complete
    - {"type": "error", "content": "..."} - Error message
    - {"type": "pong"} - Keep-alive pong
    """
    if not validate_project_name(project_name):
        await websocket.close(code=4000, reason="Invalid project name")
        return

    # Look up project directory from registry
    project_dir = _get_project_path(project_name)
    if not project_dir:
        await websocket.close(code=4004, reason="Project not found in registry")
        return

    if not project_dir.exists():
        await websocket.close(code=4004, reason="Project directory not found")
        return

    await websocket.accept()

    session: Optional[TerminalSession] = None

    try:
        while True:
            try:
                # Receive message from client
                data = await websocket.receive_text()
                message = json.loads(data)
                msg_type = message.get("type")

                if msg_type == "ping":
                    await websocket.send_json({"type": "pong"})
                    continue

                elif msg_type == "start":
                    # Get or create terminal session
                    session = await get_or_create_terminal_session(
                        project_name,
                        project_dir,
                    )

                    # Stream the initial greeting
                    async for chunk in session.start():
                        await websocket.send_json(chunk)

                elif msg_type == "message":
                    # User sent a message
                    if not session:
                        session = await get_or_create_terminal_session(
                            project_name,
                            project_dir,
                        )

                    user_content = message.get("content", "").strip()

                    if not user_content:
                        await websocket.send_json({
                            "type": "error",
                            "content": "Empty message"
                        })
                        continue

                    # Stream Claude's response
                    async for chunk in session.send_message(user_content):
                        await websocket.send_json(chunk)

                elif msg_type == "get_resources":
                    # Return current resources
                    manager = get_manager()
                    resources = manager.get_merged_resources(project_name, project_dir)
                    await websocket.send_json({
                        "type": "resources",
                        "data": {
                            name: r.to_dict()
                            for name, r in resources.items()
                        }
                    })

                else:
                    await websocket.send_json({
                        "type": "error",
                        "content": f"Unknown message type: {msg_type}"
                    })

            except json.JSONDecodeError:
                await websocket.send_json({
                    "type": "error",
                    "content": "Invalid JSON message"
                })
                continue

    except WebSocketDisconnect:
        logger.info(f"Terminal WebSocket disconnected for project: {project_name}")
    except Exception as e:
        logger.exception(f"Terminal WebSocket error for project {project_name}: {e}")
        try:
            await websocket.send_json({
                "type": "error",
                "content": f"Server error: {str(e)}"
            })
        except Exception:
            pass
    finally:
        # Don't close the session on disconnect - allow reconnection
        # Session can be explicitly closed via REST endpoint
        pass

+    Sessions are PERSISTENT - they survive UI disconnects and can be resumed.

+    When reconnecting, the full message history is restored.

-    - {"type": "start"} - Start the terminal session

+    - {"type": "start"} - Start or resume terminal session (restores history if exists)

+    - {"type": "new_session"} - Force a new session, discarding history

+    - {"type": "get_history"} - Request message history without restarting

+    - {"type": "history", "role": "...", "content": "..."} - Historical message (on reconnect)

+    - {"type": "session_restored", "message_count": N} - Session was restored with N messages


Now I need to update the message handling in the WebSocket handler to support the new message types.


                elif msg_type == "start":
                    # Get or create terminal session
                    session = await get_or_create_terminal_session(
                        project_name,
                        project_dir,
                    )

                    # Stream the initial greeting
                    async for chunk in session.start():
                        await websocket.send_json(chunk)

                elif msg_type == "start":
                    # Get or create terminal session (will restore history if exists)
                    session = await get_or_create_terminal_session(
                        project_name,
                        project_dir,
                    )

                    # Stream the initial greeting or restored history
                    async for chunk in session.start(force_new=False):
                        await websocket.send_json(chunk)

                elif msg_type == "new_session":
                    # Force a new session, discarding history
                    await close_terminal_session(project_name)
                    session = await get_or_create_terminal_session(
                        project_name,
                        project_dir,
                    )

                    # Stream the initial greeting
                    async for chunk in session.start(force_new=True):
                        await websocket.send_json(chunk)

                elif msg_type == "get_history":
                    # Return message history without restarting
                    session = await get_or_create_terminal_session(
                        project_name,
                        project_dir,
                    )

                    history = session.get_message_history()
                    await websocket.send_json({
                        "type": "history_response",
                        "messages": history,
                        "is_active": session.is_active(),
                    })


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\server\routers\terminal.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   342→                if msg_type == "ping":
   343→                    await websocket.send_json({"type": "pong"})
   344→                    continue
   345→
   346→                elif msg_type == "start":
   347→                    # Get or create terminal session (will restore history if exists)
   348→                    session = await get_or_create_terminal_session(
   349→                        project_name,
   350→                        project_dir,
   351→                    )
   352→
   353→                    # Stream the initial greeting or restored history
   354→                    async for chunk in session.start(force_new=False):
   355→                        await websocket.send_json(chunk)
   356→
   357→                elif msg_type == "new_session":
   358→                    # Force a new session, discarding history
   359→                    await close_terminal_session(project_name)
   360→                    session = await get_or_create_terminal_session(
   361→                        project_name,
   362→                        project_dir,
   363→                    )
   364→
   365→                    # Stream the initial greeting
   366→                    async for chunk in session.start(force_new=True):
   367→                        await websocket.send_json(chunk)
   368→
   369→                elif msg_type == "get_history":
   370→                    # Return message history without restarting
   371→                    session = await get_or_create_terminal_session(
   372→                        project_name,
   373→                        project_dir,
   374→                    )
   375→
   376→                    history = session.get_message_history()
   377→                    await websocket.send_json({
   378→                        "type": "history_response",
   379→                        "messages": history,
   380→                        "is_active": session.is_active(),
   381→                    })
   382→
   383→                elif msg_type == "message":
   384→                    # User sent a message
   385→                    if not session:

                elif msg_type == "start":
                    # Get or create terminal session (will restore history if exists)
                    session = await get_or_create_terminal_session(
                        project_name,
                        project_dir,
                    )

                    # Stream the initial greeting or restored history
                    async for chunk in session.start(force_new=False):
                        await websocket.send_json(chunk)

                elif msg_type == "new_session":
                    # Force a new session, discarding history
                    await close_terminal_session(project_name)
                    session = await get_or_create_terminal_session(
                        project_name,
                        project_dir,
                    )

                    # Stream the initial greeting
                    async for chunk in session.start(force_new=True):
                        await websocket.send_json(chunk)

                elif msg_type == "get_history":
                    # Return message history without restarting
                    session = await get_or_create_terminal_session(
                        project_name,
                        project_dir,
                    )

                    history = session.get_message_history()
                    await websocket.send_json({
                        "type": "history_response",
                        "messages": history,
                        "is_active": session.is_active(),
                    })

"""
Terminal Router
===============

WebSocket and REST endpoints for interactive Claude terminal sessions.
The terminal provides full system access for resource management and
infrastructure configuration.
"""

import json
import logging
import re
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
from pydantic import BaseModel

from ..services.terminal_session import (
    TerminalSession,
    get_or_create_terminal_session,
    close_terminal_session,
    get_active_sessions,
)
from ..services.resource_manager import (
    get_manager,
    Resource,
    ResourceType,
    ResourceScope,
)

logger = logging.getLogger(__name__)

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

# Root directory
ROOT_DIR = Path(__file__).parent.parent.parent

def _get_project_path(project_name: str) -> Path:
    """Get project path from registry."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import get_project_path
    return get_project_path(project_name)

def validate_project_name(name: str) -> bool:
    """Validate project name to prevent path traversal."""
    return bool(re.match(r'^[a-zA-Z0-9_-]{1,50}$', name))

# ============================================================================
# REST Endpoints
# ============================================================================

class TerminalSessionStatus(BaseModel):
    """Status of a terminal session."""
    project_name: str
    is_active: bool
    message_count: int
    resource_count: int

class ResourceInfo(BaseModel):
    """Resource information."""
    name: str
    type: str
    scope: str
    host: Optional[str] = None
    url: Optional[str] = None
    verified: bool = False

@router.get("/sessions", response_model=list[str])
async def list_terminal_sessions():
    """List all active terminal sessions."""
    return get_active_sessions()

@router.get("/sessions/{project_name}", response_model=TerminalSessionStatus)
async def get_terminal_status(project_name: str):
    """Get status of a terminal session."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    active = get_active_sessions()
    if project_name not in active:
        raise HTTPException(status_code=404, detail="No active terminal session")

    # Get resource count
    project_dir = _get_project_path(project_name)
    manager = get_manager()
    resources = manager.get_merged_resources(project_name, project_dir)

    return TerminalSessionStatus(
        project_name=project_name,
        is_active=True,
        message_count=0,  # Would need to track this in session
        resource_count=len(resources),
    )

@router.delete("/sessions/{project_name}")
async def close_session(project_name: str):
    """Close and remove a terminal session."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    await close_terminal_session(project_name)
    return {"success": True, "message": "Session closed"}

@router.get("/resources/{project_name}", response_model=list[ResourceInfo])
async def list_resources(project_name: str):
    """List resources for a project (merged global + project)."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    manager = get_manager()
    resources = manager.get_merged_resources(project_name, project_dir)

    return [
        ResourceInfo(
            name=r.name,
            type=r.type.value,
            scope=r.scope.value,
            host=r.host,
            url=r.url,
            verified=r.verified_at is not None,
        )
        for r in resources.values()
    ]

@router.get("/resources/global", response_model=list[ResourceInfo])
async def list_global_resources():
    """List all global resources."""
    manager = get_manager()
    manager._ensure_global_loaded()

    return [
        ResourceInfo(
            name=r.name,
            type=r.type.value,
            scope=r.scope.value,
            host=r.host,
            url=r.url,
            verified=r.verified_at is not None,
        )
        for r in manager._global_resources.values()
    ]

class AddResourceRequest(BaseModel):
    """Request to add a resource."""
    name: str
    type: str  # server, service, database, repository
    scope: str  # global, project
    host: Optional[str] = None
    port: Optional[int] = None
    url: Optional[str] = None
    user: Optional[str] = None
    credential_ref: Optional[str] = None
    description: Optional[str] = None

@router.post("/resources/{project_name}")
async def add_resource(project_name: str, request: AddResourceRequest):
    """Add a resource to a project or globally."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    try:
        resource_type = ResourceType(request.type)
    except ValueError:
        raise HTTPException(status_code=400, detail=f"Invalid resource type: {request.type}")

    try:
        scope = ResourceScope(request.scope)
    except ValueError:
        raise HTTPException(status_code=400, detail=f"Invalid scope: {request.scope}")

    from ..services.resource_manager import ResourceAuth, AuthType

    # Determine auth type based on credential_ref
    auth = ResourceAuth()
    if request.credential_ref:
        # Determine auth type based on resource type
        if resource_type == ResourceType.SERVER:
            auth = ResourceAuth(type=AuthType.SSH_PASSWORD, credential_ref=request.credential_ref)
        elif resource_type == ResourceType.SERVICE:
            auth = ResourceAuth(type=AuthType.API_TOKEN, credential_ref=request.credential_ref)

    resource = Resource(
        name=request.name,
        type=resource_type,
        scope=scope,
        host=request.host,
        port=request.port,
        url=request.url,
        user=request.user,
        auth=auth,
        description=request.description,
    )

    manager = get_manager()
    manager.add_resource(
        resource,
        project_name=project_name if scope == ResourceScope.PROJECT else None,
        project_dir=project_dir if scope == ResourceScope.PROJECT else None,
    )

    return {"success": True, "resource": resource.to_dict()}

@router.delete("/resources/{project_name}/{resource_name}")
async def remove_resource(project_name: str, resource_name: str, scope: str = "project"):
    """Remove a resource."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    try:
        resource_scope = ResourceScope(scope)
    except ValueError:
        raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}")

    manager = get_manager()
    removed = manager.remove_resource(
        resource_name,
        resource_scope,
        project_name=project_name if resource_scope == ResourceScope.PROJECT else None,
        project_dir=project_dir if resource_scope == ResourceScope.PROJECT else None,
    )

    if not removed:
        raise HTTPException(status_code=404, detail="Resource not found")

    return {"success": True, "message": f"Resource '{resource_name}' removed"}

@router.post("/resources/{project_name}/{resource_name}/verify")
async def verify_resource(project_name: str, resource_name: str):
    """Verify a resource is accessible."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    manager = get_manager()
    resource = manager.get_resource(resource_name, project_name, project_dir)

    if not resource:
        raise HTTPException(status_code=404, detail="Resource not found")

    result = await manager.verify_resource(resource)

    return {
        "success": result.success,
        "message": result.message,
        "capabilities": result.capabilities,
        "response_time_ms": result.response_time_ms,
    }

# ============================================================================
# WebSocket Endpoint
# ============================================================================

@router.websocket("/ws/{project_name}")
async def terminal_websocket(websocket: WebSocket, project_name: str):
    """
    WebSocket endpoint for interactive terminal chat.

    Sessions are PERSISTENT - they survive UI disconnects and can be resumed.
    When reconnecting, the full message history is restored.

    Message protocol:

    Client -> Server:
    - {"type": "start"} - Start or resume terminal session (restores history if exists)
    - {"type": "new_session"} - Force a new session, discarding history
    - {"type": "message", "content": "..."} - Send user message
    - {"type": "get_history"} - Request message history without restarting
    - {"type": "ping"} - Keep-alive ping

    Server -> Client:
    - {"type": "text", "content": "..."} - Text chunk from Claude
    - {"type": "history", "role": "...", "content": "..."} - Historical message (on reconnect)
    - {"type": "session_restored", "message_count": N} - Session was restored with N messages
    - {"type": "resources", "data": {...}} - Resource list update
    - {"type": "resource_updated", "path": "..."} - Resource file updated
    - {"type": "command_executing", "command": "..."} - Bash command being run
    - {"type": "connection_verified", "result": "..."} - Connection test result
    - {"type": "response_done"} - Response complete
    - {"type": "error", "content": "..."} - Error message
    - {"type": "pong"} - Keep-alive pong
    """
    if not validate_project_name(project_name):
        await websocket.close(code=4000, reason="Invalid project name")
        return

    # Look up project directory from registry
    project_dir = _get_project_path(project_name)
    if not project_dir:
        await websocket.close(code=4004, reason="Project not found in registry")
        return

    if not project_dir.exists():
        await websocket.close(code=4004, reason="Project directory not found")
        return

    await websocket.accept()

    session: Optional[TerminalSession] = None

    try:
        while True:
            try:
                # Receive message from client
                data = await websocket.receive_text()
                message = json.loads(data)
                msg_type = message.get("type")

                if msg_type == "ping":
                    await websocket.send_json({"type": "pong"})
                    continue

                elif msg_type == "start":
                    # Get or create terminal session
                    session = await get_or_create_terminal_session(
                        project_name,
                        project_dir,
                    )

                    # Stream the initial greeting
                    async for chunk in session.start():
                        await websocket.send_json(chunk)

                elif msg_type == "message":
                    # User sent a message
                    if not session:
                        session = await get_or_create_terminal_session(
                            project_name,
                            project_dir,
                        )

                    user_content = message.get("content", "").strip()

                    if not user_content:
                        await websocket.send_json({
                            "type": "error",
                            "content": "Empty message"
                        })
                        continue

                    # Stream Claude's response
                    async for chunk in session.send_message(user_content):
                        await websocket.send_json(chunk)

                elif msg_type == "get_resources":
                    # Return current resources
                    manager = get_manager()
                    resources = manager.get_merged_resources(project_name, project_dir)
                    await websocket.send_json({
                        "type": "resources",
                        "data": {
                            name: r.to_dict()
                            for name, r in resources.items()
                        }
                    })

                else:
                    await websocket.send_json({
                        "type": "error",
                        "content": f"Unknown message type: {msg_type}"
                    })

            except json.JSONDecodeError:
                await websocket.send_json({
                    "type": "error",
                    "content": "Invalid JSON message"
                })
                continue

    except WebSocketDisconnect:
        logger.info(f"Terminal WebSocket disconnected for project: {project_name}")
    except Exception as e:
        logger.exception(f"Terminal WebSocket error for project {project_name}: {e}")
        try:
            await websocket.send_json({
                "type": "error",
                "content": f"Server error: {str(e)}"
            })
        except Exception:
            pass
    finally:
        # Don't close the session on disconnect - allow reconnection
        # Session can be explicitly closed via REST endpoint
        pass

-                    # Get or create terminal session

+                    # Get or create terminal session (will restore history if exists)

+                    # Stream the initial greeting or restored history

+                    async for chunk in session.start(force_new=False):

+                        await websocket.send_json(chunk)

+                elif msg_type == "new_session":

+                    # Force a new session, discarding history

+                    await close_terminal_session(project_name)

+                    session = await get_or_create_terminal_session(

-                    async for chunk in session.start():

+                    async for chunk in session.start(force_new=True):

+                elif msg_type == "get_history":

+                    # Return message history without restarting

+                    history = session.get_message_history()

+                    await websocket.send_json({

+                        "type": "history_response",

+                        "messages": history,

+                        "is_active": session.is_active(),


Now I need to update the UI to handle the new message types. Let me look at the useTerminal hook.


TodoWrite

Add session history restoration to terminal backend

Update UI to restore messages on reconnect

Update APP_SPEC_CONVERSATION.md with terminal fix


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

Add session history restoration to terminal backend

Update UI to restore messages on reconnect

Update APP_SPEC_CONVERSATION.md with terminal fix


Let me read the useTerminal hook to see what needs to be updated on the UI side.