     1→"""
     2→Terminal Router
     3→===============
     4→
     5→WebSocket and REST endpoints for interactive Claude terminal sessions.
     6→The terminal provides full system access for resource management and
     7→infrastructure configuration.
     8→"""
     9→
    10→import json
    11→import logging
    12→import re
    13→from pathlib import Path
    14→from typing import Optional
    15→
    16→from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
    17→from pydantic import BaseModel
    18→
    19→from ..services.terminal_session import (
    20→    TerminalSession,
    21→    get_or_create_terminal_session,
    22→    close_terminal_session,
    23→    get_active_sessions,
    24→)
    25→from ..services.resource_manager import (
    26→    get_manager,
    27→    Resource,
    28→    ResourceType,
    29→    ResourceScope,
    30→)
    31→
    32→logger = logging.getLogger(__name__)
    33→
    34→router = APIRouter(prefix="/api/terminal", tags=["terminal"])
    35→
    36→# Root directory
    37→ROOT_DIR = Path(__file__).parent.parent.parent
    38→
    39→
    40→def _get_project_path(project_name: str) -> Path:
    41→    """Get project path from registry."""
    42→    import sys
    43→    root = Path(__file__).parent.parent.parent
    44→    if str(root) not in sys.path:
    45→        sys.path.insert(0, str(root))
    46→
    47→    from registry import get_project_path
    48→    return get_project_path(project_name)
    49→
    50→
    51→def validate_project_name(name: str) -> bool:
    52→    """Validate project name to prevent path traversal."""
    53→    return bool(re.match(r'^[a-zA-Z0-9_-]{1,50}$', name))
    54→
    55→
    56→# ============================================================================
    57→# REST Endpoints
    58→# ============================================================================
    59→
    60→class TerminalSessionStatus(BaseModel):
    61→    """Status of a terminal session."""
    62→    project_name: str
    63→    is_active: bool
    64→    message_count: int
    65→    resource_count: int
    66→
    67→
    68→class ResourceInfo(BaseModel):
    69→    """Resource information."""
    70→    name: str
    71→    type: str
    72→    scope: str
    73→    host: Optional[str] = None
    74→    url: Optional[str] = None
    75→    verified: bool = False
    76→
    77→
    78→@router.get("/sessions", response_model=list[str])
    79→async def list_terminal_sessions():
    80→    """List all active terminal sessions."""
    81→    return get_active_sessions()
    82→
    83→
    84→@router.get("/sessions/{project_name}", response_model=TerminalSessionStatus)
    85→async def get_terminal_status(project_name: str):
    86→    """Get status of a terminal session."""
    87→    if not validate_project_name(project_name):
    88→        raise HTTPException(status_code=400, detail="Invalid project name")
    89→
    90→    active = get_active_sessions()
    91→    if project_name not in active:
    92→        raise HTTPException(status_code=404, detail="No active terminal session")
    93→
    94→    # Get resource count
    95→    project_dir = _get_project_path(project_name)
    96→    manager = get_manager()
    97→    resources = manager.get_merged_resources(project_name, project_dir)
    98→
    99→    return TerminalSessionStatus(
   100→        project_name=project_name,
   101→        is_active=True,
   102→        message_count=0,  # Would need to track this in session
   103→        resource_count=len(resources),
   104→    )
   105→
   106→
   107→@router.delete("/sessions/{project_name}")
   108→async def close_session(project_name: str):
   109→    """Close and remove a terminal session."""
   110→    if not validate_project_name(project_name):
   111→        raise HTTPException(status_code=400, detail="Invalid project name")
   112→
   113→    await close_terminal_session(project_name)
   114→    return {"success": True, "message": "Session closed"}
   115→
   116→
   117→@router.get("/resources/{project_name}", response_model=list[ResourceInfo])
   118→async def list_resources(project_name: str):
   119→    """List resources for a project (merged global + project)."""
   120→    if not validate_project_name(project_name):
   121→        raise HTTPException(status_code=400, detail="Invalid project name")
   122→
   123→    project_dir = _get_project_path(project_name)
   124→    if not project_dir:
   125→        raise HTTPException(status_code=404, detail="Project not found")
   126→
   127→    manager = get_manager()
   128→    resources = manager.get_merged_resources(project_name, project_dir)
   129→
   130→    return [
   131→        ResourceInfo(
   132→            name=r.name,
   133→            type=r.type.value,
   134→            scope=r.scope.value,
   135→            host=r.host,
   136→            url=r.url,
   137→            verified=r.verified_at is not None,
   138→        )
   139→        for r in resources.values()
   140→    ]
   141→
   142→
   143→@router.get("/resources/global", response_model=list[ResourceInfo])
   144→async def list_global_resources():
   145→    """List all global resources."""
   146→    manager = get_manager()
   147→    manager._ensure_global_loaded()
   148→
   149→    return [
   150→        ResourceInfo(
   151→            name=r.name,
   152→            type=r.type.value,
   153→            scope=r.scope.value,
   154→            host=r.host,
   155→            url=r.url,
   156→            verified=r.verified_at is not None,
   157→        )
   158→        for r in manager._global_resources.values()
   159→    ]
   160→
   161→
   162→class AddResourceRequest(BaseModel):
   163→    """Request to add a resource."""
   164→    name: str
   165→    type: str  # server, service, database, repository
   166→    scope: str  # global, project
   167→    host: Optional[str] = None
   168→    port: Optional[int] = None
   169→    url: Optional[str] = None
   170→    user: Optional[str] = None
   171→    credential_ref: Optional[str] = None
   172→    description: Optional[str] = None
   173→
   174→
   175→@router.post("/resources/{project_name}")
   176→async def add_resource(project_name: str, request: AddResourceRequest):
   177→    """Add a resource to a project or globally."""
   178→    if not validate_project_name(project_name):
   179→        raise HTTPException(status_code=400, detail="Invalid project name")
   180→
   181→    project_dir = _get_project_path(project_name)
   182→    if not project_dir:
   183→        raise HTTPException(status_code=404, detail="Project not found")
   184→
   185→    try:
   186→        resource_type = ResourceType(request.type)
   187→    except ValueError:
   188→        raise HTTPException(status_code=400, detail=f"Invalid resource type: {request.type}")
   189→
   190→    try:
   191→        scope = ResourceScope(request.scope)
   192→    except ValueError:
   193→        raise HTTPException(status_code=400, detail=f"Invalid scope: {request.scope}")
   194→
   195→    from ..services.resource_manager import ResourceAuth, AuthType
   196→
   197→    # Determine auth type based on credential_ref
   198→    auth = ResourceAuth()
   199→    if request.credential_ref:
   200→        # Determine auth type based on resource type
   201→        if resource_type == ResourceType.SERVER:
   202→            auth = ResourceAuth(type=AuthType.SSH_PASSWORD, credential_ref=request.credential_ref)
   203→        elif resource_type == ResourceType.SERVICE:
   204→            auth = ResourceAuth(type=AuthType.API_TOKEN, credential_ref=request.credential_ref)
   205→
   206→    resource = Resource(
   207→        name=request.name,
   208→        type=resource_type,
   209→        scope=scope,
   210→        host=request.host,
   211→        port=request.port,
   212→        url=request.url,
   213→        user=request.user,
   214→        auth=auth,
   215→        description=request.description,
   216→    )
   217→
   218→    manager = get_manager()
   219→    manager.add_resource(
   220→        resource,
   221→        project_name=project_name if scope == ResourceScope.PROJECT else None,
   222→        project_dir=project_dir if scope == ResourceScope.PROJECT else None,
   223→    )
   224→
   225→    return {"success": True, "resource": resource.to_dict()}
   226→
   227→
   228→@router.delete("/resources/{project_name}/{resource_name}")
   229→async def remove_resource(project_name: str, resource_name: str, scope: str = "project"):
   230→    """Remove a resource."""
   231→    if not validate_project_name(project_name):
   232→        raise HTTPException(status_code=400, detail="Invalid project name")
   233→
   234→    project_dir = _get_project_path(project_name)
   235→    if not project_dir:
   236→        raise HTTPException(status_code=404, detail="Project not found")
   237→
   238→    try:
   239→        resource_scope = ResourceScope(scope)
   240→    except ValueError:
   241→        raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}")
   242→
   243→    manager = get_manager()
   244→    removed = manager.remove_resource(
   245→        resource_name,
   246→        resource_scope,
   247→        project_name=project_name if resource_scope == ResourceScope.PROJECT else None,
   248→        project_dir=project_dir if resource_scope == ResourceScope.PROJECT else None,
   249→    )
   250→
   251→    if not removed:
   252→        raise HTTPException(status_code=404, detail="Resource not found")
   253→
   254→    return {"success": True, "message": f"Resource '{resource_name}' removed"}
   255→
   256→
   257→@router.post("/resources/{project_name}/{resource_name}/verify")
   258→async def verify_resource(project_name: str, resource_name: str):
   259→    """Verify a resource is accessible."""
   260→    if not validate_project_name(project_name):
   261→        raise HTTPException(status_code=400, detail="Invalid project name")
   262→
   263→    project_dir = _get_project_path(project_name)
   264→    if not project_dir:
   265→        raise HTTPException(status_code=404, detail="Project not found")
   266→
   267→    manager = get_manager()
   268→    resource = manager.get_resource(resource_name, project_name, project_dir)
   269→
   270→    if not resource:
   271→        raise HTTPException(status_code=404, detail="Resource not found")
   272→
   273→    result = await manager.verify_resource(resource)
   274→
   275→    return {
   276→        "success": result.success,
   277→        "message": result.message,
   278→        "capabilities": result.capabilities,
   279→        "response_time_ms": result.response_time_ms,
   280→    }
   281→
   282→
   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→    Message protocol:
   293→
   294→    Client -> Server:
   295→    - {"type": "start"} - Start the terminal session
   296→    - {"type": "message", "content": "..."} - Send user message
   297→    - {"type": "ping"} - Keep-alive ping
   298→
   299→    Server -> Client:
   300→    - {"type": "text", "content": "..."} - Text chunk from Claude
   301→    - {"type": "resources", "data": {...}} - Resource list update
   302→    - {"type": "resource_updated", "path": "..."} - Resource file updated
   303→    - {"type": "command_executing", "command": "..."} - Bash command being run
   304→    - {"type": "connection_verified", "result": "..."} - Connection test result
   305→    - {"type": "response_done"} - Response complete
   306→    - {"type": "error", "content": "..."} - Error message
   307→    - {"type": "pong"} - Keep-alive pong
   308→    """
   309→    if not validate_project_name(project_name):
   310→        await websocket.close(code=4000, reason="Invalid project name")
   311→        return
   312→
   313→    # Look up project directory from registry
   314→    project_dir = _get_project_path(project_name)
   315→    if not project_dir:
   316→        await websocket.close(code=4004, reason="Project not found in registry")
   317→        return
   318→
   319→    if not project_dir.exists():
   320→        await websocket.close(code=4004, reason="Project directory not found")
   321→        return
   322→
   323→    await websocket.accept()
   324→
   325→    session: Optional[TerminalSession] = None
   326→
   327→    try:
   328→        while True:
   329→            try:
   330→                # Receive message from client
   331→                data = await websocket.receive_text()
   332→                message = json.loads(data)
   333→                msg_type = message.get("type")
   334→
   335→                if msg_type == "ping":
   336→                    await websocket.send_json({"type": "pong"})
   337→                    continue
   338→
   339→                elif msg_type == "start":
   340→                    # Get or create terminal session
   341→                    session = await get_or_create_terminal_session(
   342→                        project_name,
   343→                        project_dir,
   344→                    )
   345→
   346→                    # Stream the initial greeting
   347→                    async for chunk in session.start():
   348→                        await websocket.send_json(chunk)
   349→
   350→                elif msg_type == "message":
   351→                    # User sent a message
   352→                    if not session:
   353→                        session = await get_or_create_terminal_session(
   354→                            project_name,
   355→                            project_dir,
   356→                        )
   357→
   358→                    user_content = message.get("content", "").strip()
   359→
   360→                    if not user_content:
   361→                        await websocket.send_json({
   362→                            "type": "error",
   363→                            "content": "Empty message"
   364→                        })
   365→                        continue
   366→
   367→                    # Stream Claude's response
   368→                    async for chunk in session.send_message(user_content):
   369→                        await websocket.send_json(chunk)
   370→
   371→                elif msg_type == "get_resources":
   372→                    # Return current resources
   373→                    manager = get_manager()
   374→                    resources = manager.get_merged_resources(project_name, project_dir)
   375→                    await websocket.send_json({
   376→                        "type": "resources",
   377→                        "data": {
   378→                            name: r.to_dict()
   379→                            for name, r in resources.items()
   380→                        }
   381→                    })
   382→
   383→                else:
   384→                    await websocket.send_json({
   385→                        "type": "error",
   386→                        "content": f"Unknown message type: {msg_type}"
   387→                    })
   388→
   389→            except json.JSONDecodeError:
   390→                await websocket.send_json({
   391→                    "type": "error",
   392→                    "content": "Invalid JSON message"
   393→                })
   394→                continue
   395→
   396→    except WebSocketDisconnect:
   397→        logger.info(f"Terminal WebSocket disconnected for project: {project_name}")
   398→    except Exception as e:
   399→        logger.exception(f"Terminal WebSocket error for project {project_name}: {e}")
   400→        try:
   401→            await websocket.send_json({
   402→                "type": "error",
   403→                "content": f"Server error: {str(e)}"
   404→            })
   405→        except Exception:
   406→            pass
   407→    finally:
   408→        # Don't close the session on disconnect - allow reconnection
   409→        # Session can be explicitly closed via REST endpoint
   410→        pass
   411→

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

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


I see the issue now. The backend has some session persistence (`get_or_create_terminal_session` keeps sessions alive), but the problem is:

1. The session's `messages` list is stored in memory but not sent back to the UI when reconnecting
2. When the UI reconnects, it calls `start()` which creates a fresh greeting instead of restoring the conversation
3. The conversation history isn't being saved/restored

The fix needs to:
1. Store message history in the session (already done)
2. Send history back to UI when reconnecting
3. Add an endpoint or websocket message type to get history
4. Potentially persist history to disk for crash recovery

Let me also check the UI side to see how the terminal component handles this.


Let me read the TerminalChat.tsx to understand how the UI handles reconnection.


1→/**
     2→ * Terminal Chat Component
     3→ *
     4→ * Interactive Claude terminal for resource management, infrastructure
     5→ * discovery, and system administration tasks.
     6→ */
     7→
     8→import { useCallback, useEffect, useRef, useState } from 'react'
     9→import {
    10→  Send,
    11→  X,
    12→  AlertCircle,
    13→  Wifi,
    14→  WifiOff,
    15→  RotateCcw,
    16→  Server,
    17→  RefreshCw,
    18→  Terminal as TerminalIcon,
    19→  ChevronDown,
    20→  ChevronUp,
    21→} from 'lucide-react'
    22→import { useTerminal, type TerminalMessage, type Resource } from '../hooks/useTerminal'
    23→import { TypingIndicator } from './TypingIndicator'
    24→import ReactMarkdown from 'react-markdown'
    25→
    26→interface TerminalChatProps {
    27→  projectName: string
    28→  onClose: () => void
    29→}
    30→
    31→// Message component for terminal
    32→function TerminalMessageItem({ message }: { message: TerminalMessage }) {
    33→  const isUser = message.role === 'user'
    34→  const isSystem = message.role === 'system'
    35→
    36→  return (
    37→

    46→

    47→

    48→          {!isUser && !isSystem && (
    49→

    50→              <TerminalIcon size={14} className="text-white" />
    51→

    52→          )}
    53→          {isUser && (
    54→

    55→              U
    56→

    57→          )}
    58→          {isSystem && (
    59→

    60→              !
    61→

    62→          )}
    63→
    64→

    65→            {isSystem ? (
    66→              {message.content}
    67→            ) : (
    68→

    69→                <ReactMarkdown>{message.content}</ReactMarkdown>
    70→

    71→            )}
    72→            {message.isStreaming && (
    73→
    74→            )}
    75→

    76→

    77→

    78→

    79→  )
    80→}
    81→
    82→// Resource sidebar panel
    83→function ResourcePanel({
    84→  resources,
    85→  onRefresh,
    86→  isOpen,
    87→  onToggle,
    88→}: {
    89→  resources: Record<string, Resource>
    90→  onRefresh: () => void
    91→  isOpen: boolean
    92→  onToggle: () => void
    93→}) {
    94→  const resourceList = Object.values(resources)
    95→  const servers = resourceList.filter((r) => r.type === 'server')
    96→  const services = resourceList.filter((r) => r.type === 'service')
    97→  const others = resourceList.filter((r) => r.type !== 'server' && r.type !== 'service')
    98→
    99→  return (
   100→

   101→      {/* Header */}
   102→

   106→

   107→          <Server size={16} />
   108→          Resources
   109→
   110→            ({resourceList.length})
   111→
   112→

   113→

   114→          <button
   115→            onClick={(e) => {
   116→              e.stopPropagation()
   117→              onRefresh()
   118→            }}
   119→            className="p-1 hover:bg-gray-100 rounded"
   120→            title="Refresh resources"
   121→          >
   122→            <RefreshCw size={14} />
   123→          </button>
   124→          {isOpen ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
   125→

   126→

   127→
   128→      {/* Content */}
   129→      {isOpen && (
   130→

   131→          {resourceList.length === 0 ? (
   132→

   133→              No resources configured.
   134→

   135→              Use the terminal to add servers and services.
   136→

   137→          ) : (
   138→            <>
   139→              {servers.length > 0 && (
   140→

   141→

   142→                    Servers
   143→

   144→                  {servers.map((server) => (
   145→

   149→

   150→                        {server.name}
   151→                        {server.verified_at && (
   152→
   153→                        )}
   154→

   155→

   156→                        {server.host}
   157→                        {server.user && ` (${server.user})`}
   158→

   159→                      {server.capabilities && server.capabilities.length > 0 && (
   160→

   161→                          {server.capabilities.map((cap) => (
   162→
   166→                              {cap}
   167→
   168→                          ))}
   169→

   170→                      )}
   171→

   172→                        {server.scope}
   173→

   174→

   175→                  ))}
   176→

   177→              )}
   178→
   179→              {services.length > 0 && (
   180→

   181→

   182→                    Services
   183→

   184→                  {services.map((service) => (
   185→

   189→
{service.name}

   190→

   191→                        {service.url}
   192→

   193→

   194→                        {service.scope}
   195→

   196→

   197→                  ))}
   198→

   199→              )}
   200→
   201→              {others.length > 0 && (
   202→

   203→

   204→                    Other
   205→

   206→                  {others.map((resource) => (
   207→

   211→
{resource.name}

   212→

   213→                        {resource.type}
   214→

   215→

   216→                  ))}
   217→

   218→              )}
   219→            </>
   220→          )}
   221→

   222→      )}
   223→

   224→  )
   225→}
   226→
   227→export function TerminalChat({ projectName, onClose }: TerminalChatProps) {
   228→  const [input, setInput] = useState('')
   229→  const [error, setError] = useState<string | null>(null)
   230→  const [resourcePanelOpen, setResourcePanelOpen] = useState(true)
   231→  const messagesEndRef = useRef<HTMLDivElement>(null)
   232→  const inputRef = useRef<HTMLInputElement>(null)
   233→
   234→  const {
   235→    messages,
   236→    resources,
   237→    isLoading,
   238→    connectionStatus,
   239→    start,
   240→    sendMessage,
   241→    disconnect,
   242→    refreshResources,
   243→  } = useTerminal({
   244→    projectName,
   245→    onError: (err) => setError(err),
   246→    onResourcesUpdated: () => {
   247→      // Resources updated
   248→    },
   249→  })
   250→
   251→  // Start the terminal session when component mounts
   252→  useEffect(() => {
   253→    start()
   254→
   255→    return () => {
   256→      disconnect()
   257→    }
   258→  }, []) // eslint-disable-line react-hooks/exhaustive-deps
   259→
   260→  // Scroll to bottom when messages change
   261→  useEffect(() => {
   262→    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
   263→  }, [messages])
   264→
   265→  // Focus input after message is sent
   266→  useEffect(() => {
   267→    if (!isLoading) {
   268→      inputRef.current?.focus()
   269→    }
   270→  }, [isLoading])
   271→
   272→  const handleSubmit = useCallback(
   273→    (e: React.FormEvent) => {
   274→      e.preventDefault()
   275→      const trimmed = input.trim()
   276→      if (!trimmed || isLoading) return
   277→
   278→      sendMessage(trimmed)
   279→      setInput('')
   280→    },
   281→    [input, isLoading, sendMessage]
   282→  )
   283→
   284→  const handleKeyDown = useCallback(
   285→    (e: React.KeyboardEvent) => {
   286→      if (e.key === 'Enter' && !e.shiftKey) {
   287→        e.preventDefault()
   288→        handleSubmit(e)
   289→      }
   290→    },
   291→    [handleSubmit]
   292→  )
   293→
   294→  // Connection status indicator
   295→  const ConnectionIndicator = () => {
   296→    switch (connectionStatus) {
   297→      case 'connected':
   298→        return (
   299→
   300→            <Wifi size={12} />
   301→            Connected
   302→
   303→        )
   304→      case 'connecting':
   305→        return (
   306→
   307→            <Wifi size={12} className="animate-pulse" />
   308→            Connecting...
   309→
   310→        )
   311→      case 'error':
   312→        return (
   313→
   314→            <WifiOff size={12} />
   315→            Error
   316→
   317→        )
   318→      default:
   319→        return (
   320→
   321→            <WifiOff size={12} />
   322→            Disconnected
   323→
   324→        )
   325→    }
   326→  }
   327→
   328→  return (
   329→

   330→      {/* Header */}
   331→

   332→

   333→          <TerminalIcon size={20} />
   334→

   335→            Terminal: {projectName}
   336→

   337→          <ConnectionIndicator />
   338→

   339→
   340→        <button
   341→          onClick={onClose}
   342→          className="neo-btn neo-btn-ghost p-2"
   343→          title="Close Terminal"
   344→        >
   345→          <X size={20} />
   346→        </button>
   347→

   348→
   349→      {/* Error banner */}
   350→      {error && (
   351→

   352→          <AlertCircle size={16} />
   353→          {error}
   354→          <button
   355→            onClick={() => setError(null)}
   356→            className="p-1 hover:bg-white/20 rounded"
   357→          >
   358→            <X size={14} />
   359→          </button>
   360→

   361→      )}
   362→
   363→      {/* Main content area */}
   364→

   365→        {/* Messages area */}
   366→

   367→

   368→            {messages.length === 0 && !isLoading && (
   369→

   370→

   371→

   372→                    Resource Terminal
   373→

   374→

   375→                    Use this terminal to manage infrastructure resources.
   376→

   377→

   378→                    Try commands like:
   379→

   380→

   381→
"Add my build server at 172.16.3.30"

   382→
"Test connection to jupiter"

   383→
"What servers are configured?"

   384→
"Add Gitea as a global service"

   385→

   386→                  {connectionStatus === 'error' && (
   387→                    <button
   388→                      onClick={start}
   389→                      className="neo-btn neo-btn-primary mt-4 text-sm"
   390→                    >
   391→                      <RotateCcw size={14} />
   392→                      Retry Connection
   393→                    </button>
   394→                  )}
   395→

   396→

   397→            )}
   398→
   399→            {messages.map((message) => (
   400→              <TerminalMessageItem key={message.id} message={message} />
   401→            ))}
   402→
   403→            {isLoading && <TypingIndicator />}
   404→
   405→

   406→

   407→
   408→          {/* Input area */}
   409→

   410→            <form onSubmit={handleSubmit} className="flex gap-2">
   411→              <input
   412→                ref={inputRef}
   413→                type="text"
   414→                value={input}
   415→                onChange={(e) => setInput(e.target.value)}
   416→                onKeyDown={handleKeyDown}
   417→                placeholder="Type a command or question..."
   418→                disabled={isLoading || connectionStatus !== 'connected'}
   419→                className="flex-1 neo-input font-mono text-sm"
   420→                autoFocus
   421→              />
   422→              <button
   423→                type="submit"
   424→                disabled={isLoading || !input.trim() || connectionStatus !== 'connected'}
   425→                className="neo-btn neo-btn-primary"
   426→                title="Send"
   427→              >
   428→                <Send size={18} />
   429→              </button>
   430→            </form>
   431→

   432→

   433→
   434→        {/* Resource sidebar */}
   435→        <ResourcePanel
   436→          resources={resources}
   437→          onRefresh={refreshResources}
   438→          isOpen={resourcePanelOpen}
   439→          onToggle={() => setResourcePanelOpen(!resourcePanelOpen)}
   440→        />
   441→

   442→

   443→  )
   444→}
   445→

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

/**
 * Terminal Chat Component
 *
 * Interactive Claude terminal for resource management, infrastructure
 * discovery, and system administration tasks.
 */

import { useCallback, useEffect, useRef, useState } from 'react'
import {
  Send,
  X,
  AlertCircle,
  Wifi,
  WifiOff,
  RotateCcw,
  Server,
  RefreshCw,
  Terminal as TerminalIcon,
  ChevronDown,
  ChevronUp,
} from 'lucide-react'
import { useTerminal, type TerminalMessage, type Resource } from '../hooks/useTerminal'
import { TypingIndicator } from './TypingIndicator'
import ReactMarkdown from 'react-markdown'

interface TerminalChatProps {
  projectName: string
  onClose: () => void
}

// Message component for terminal
function TerminalMessageItem({ message }: { message: TerminalMessage }) {
  const isUser = message.role === 'user'
  const isSystem = message.role === 'system'

  return (

          {!isUser && !isSystem && (

              <TerminalIcon size={14} className="text-white" />

          )}
          {isUser && (

              U

          )}
          {isSystem && (

              !

          )}

            {isSystem ? (
              {message.content}
            ) : (

                <ReactMarkdown>{message.content}</ReactMarkdown>

            )}
            {message.isStreaming && (

            )}

  )
}

// Resource sidebar panel
function ResourcePanel({
  resources,
  onRefresh,
  isOpen,
  onToggle,
}: {
  resources: Record<string, Resource>
  onRefresh: () => void
  isOpen: boolean
  onToggle: () => void
}) {
  const resourceList = Object.values(resources)
  const servers = resourceList.filter((r) => r.type === 'server')
  const services = resourceList.filter((r) => r.type === 'service')
  const others = resourceList.filter((r) => r.type !== 'server' && r.type !== 'service')

  return (

      {/* Header */}

          <Server size={16} />
          Resources

            ({resourceList.length})

          <button
            onClick={(e) => {
              e.stopPropagation()
              onRefresh()
            }}
            className="p-1 hover:bg-gray-100 rounded"
            title="Refresh resources"
          >
            <RefreshCw size={14} />
          </button>
          {isOpen ? <ChevronUp size={16} /> : <ChevronDown size={16} />}

      {/* Content */}
      {isOpen && (

          {resourceList.length === 0 ? (

              No resources configured.

              Use the terminal to add servers and services.

          ) : (
            <>
              {servers.length > 0 && (

                    Servers

                  {servers.map((server) => (

                        {server.name}
                        {server.verified_at && (

                        )}

                        {server.host}
                        {server.user && ` (${server.user})`}

                      {server.capabilities && server.capabilities.length > 0 && (

                          {server.capabilities.map((cap) => (

                              {cap}

                          ))}

                      )}

                        {server.scope}

                  ))}

              )}

              {services.length > 0 && (

                    Services

                  {services.map((service) => (

{service.name}

                        {service.url}

                        {service.scope}

                  ))}

              )}

              {others.length > 0 && (

                    Other

                  {others.map((resource) => (

{resource.name}

                        {resource.type}

                  ))}

              )}
            </>
          )}

      )}

  )
}

export function TerminalChat({ projectName, onClose }: TerminalChatProps) {
  const [input, setInput] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [resourcePanelOpen, setResourcePanelOpen] = useState(true)
  const messagesEndRef = useRef<HTMLDivElement>(null)
  const inputRef = useRef<HTMLInputElement>(null)

  const {
    messages,
    resources,
    isLoading,
    connectionStatus,
    start,
    sendMessage,
    disconnect,
    refreshResources,
  } = useTerminal({
    projectName,
    onError: (err) => setError(err),
    onResourcesUpdated: () => {
      // Resources updated
    },
  })

  // Start the terminal session when component mounts
  useEffect(() => {
    start()

    return () => {
      disconnect()
    }
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  // Scroll to bottom when messages change
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  // Focus input after message is sent
  useEffect(() => {
    if (!isLoading) {
      inputRef.current?.focus()
    }
  }, [isLoading])

  const handleSubmit = useCallback(
    (e: React.FormEvent) => {
      e.preventDefault()
      const trimmed = input.trim()
      if (!trimmed || isLoading) return

      sendMessage(trimmed)
      setInput('')
    },
    [input, isLoading, sendMessage]
  )

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault()
        handleSubmit(e)
      }
    },
    [handleSubmit]
  )

  // Connection status indicator
  const ConnectionIndicator = () => {
    switch (connectionStatus) {
      case 'connected':
        return (

            <Wifi size={12} />
            Connected

        )
      case 'connecting':
        return (

            <Wifi size={12} className="animate-pulse" />
            Connecting...

        )
      case 'error':
        return (

            <WifiOff size={12} />
            Error

        )
      default:
        return (

            <WifiOff size={12} />
            Disconnected

        )
    }
  }

  return (

      {/* Header */}

          <TerminalIcon size={20} />

            Terminal: {projectName}

          <ConnectionIndicator />

        <button
          onClick={onClose}
          className="neo-btn neo-btn-ghost p-2"
          title="Close Terminal"
        >
          <X size={20} />
        </button>

      {/* Error banner */}
      {error && (

          <AlertCircle size={16} />
          {error}
          <button
            onClick={() => setError(null)}
            className="p-1 hover:bg-white/20 rounded"
          >
            <X size={14} />
          </button>

      )}

      {/* Main content area */}

        {/* Messages area */}

            {messages.length === 0 && !isLoading && (

                    Resource Terminal

                    Use this terminal to manage infrastructure resources.

                    Try commands like:

"Add my build server at 172.16.3.30"

"Test connection to jupiter"

"What servers are configured?"

"Add Gitea as a global service"

                  {connectionStatus === 'error' && (
                    <button
                      onClick={start}
                      className="neo-btn neo-btn-primary mt-4 text-sm"
                    >
                      <RotateCcw size={14} />
                      Retry Connection
                    </button>
                  )}

            )}

            {messages.map((message) => (
              <TerminalMessageItem key={message.id} message={message} />
            ))}

            {isLoading && <TypingIndicator />}

          {/* Input area */}

            <form onSubmit={handleSubmit} className="flex gap-2">
              <input
                ref={inputRef}
                type="text"
                value={input}
                onChange={(e) => setInput(e.target.value)}
                onKeyDown={handleKeyDown}
                placeholder="Type a command or question..."
                disabled={isLoading || connectionStatus !== 'connected'}
                className="flex-1 neo-input font-mono text-sm"
                autoFocus
              />
              <button
                type="submit"
                disabled={isLoading || !input.trim() || connectionStatus !== 'connected'}
                className="neo-btn neo-btn-primary"
                title="Send"
              >
                <Send size={18} />
              </button>
            </form>

        {/* Resource sidebar */}
        <ResourcePanel
          resources={resources}
          onRefresh={refreshResources}
          isOpen={resourcePanelOpen}
          onToggle={() => setResourcePanelOpen(!resourcePanelOpen)}
        />

  )
}


Now I understand the full picture. The issue is:

1. **Backend**: Sessions are kept in memory (`_sessions` dict) and preserve their Claude client + message history
2. **Problem 1**: When WebSocket reconnects, it calls `session.start()` which sends a fresh greeting instead of restoring history
3. **Problem 2**: The UI component clears its local `messages` state when unmounting (line 256: `disconnect()`)
4. **Problem 3**: There's no mechanism to get the message history from the backend when reconnecting

The fix needs:

1. **Backend changes:**
   - Add a way to restore session with history instead of fresh start
   - Maybe add `get_history` websocket message type or REST endpoint
   - Store a flag in session to indicate if it was already started

2. **UI changes:**
   - When reconnecting, fetch history from backend
   - Don't clear messages when component unmounts (or persist them)

Let me implement this fix.


Found the issue. The backend keeps sessions alive but:
1. When reconnecting, `start()` sends a fresh greeting instead of restoring history
2. The UI clears its message state on unmount
3. There's no mechanism to retrieve message history on reconnect

Let me fix this.


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


Now let me implement the fix. I'll:

1. Modify `terminal_session.py` to:
   - Track if session has been started
   - Add a method to get history
   - Modify start() to return history if already started

2. Modify `terminal.py` router to:
   - Add a "get_history" message type
   - Handle reconnection properly

3. Modify the UI hook to request history on reconnect


Edit

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

    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._resource_manager = get_manager()
        self._credential_resolver = get_resolver()

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


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:
   100→{CWD}
   101→"""
   102→
   103→
   104→class TerminalSession:
   105→    """
   106→    Manages an interactive Claude terminal session for one project.
   107→
   108→    The terminal provides full system access for:
   109→    - Resource management (add servers, services)
   110→    - Connection testing and capability probing
   111→    - Infrastructure configuration
   112→
   113→    Sessions are persistent - they survive UI disconnects and can be resumed.
   114→    """
   115→
   116→    def __init__(
   117→        self,
   118→        project_name: str,
   119→        project_dir: Path,
   120→    ):
   121→        """
   122→        Initialize the terminal session.
   123→
   124→        Args:
   125→            project_name: Name of the project
   126→            project_dir: Absolute path to the project directory
   127→        """
   128→        self.project_name = project_name
   129→        self.project_dir = project_dir
   130→        self.client: Optional[ClaudeSDKClient] = None
   131→        self.messages: list[dict] = []
   132→        self.created_at = datetime.now()
   133→        self._client_entered: bool = False
   134→        self._session_started: bool = False  # Track if initial greeting was sent
   135→        self._resource_manager = get_manager()
   136→        self._credential_resolver = get_resolver()
   137→
   138→    def get_resources(self) -> dict:
   139→        """Get current resources for the project."""
   140→        resources = self._resource_manager.get_merged_resources(

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

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

    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._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_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())

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

+        self._session_started: bool = False  # Track if initial greeting was sent


Good, now I need to add a method to get message history and modify the start() method to handle reconnection.


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

    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}**"]


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:
   155→            finally:
   156→                self._client_entered = False
   157→                self.client = None
   158→
   159→    def get_message_history(self) -> list[dict]:
   160→        """
   161→        Get the message history for this session.
   162→
   163→        Returns:
   164→            List of message dicts with role, content, timestamp
   165→        """
   166→        return self.messages.copy()
   167→
   168→    def is_active(self) -> bool:
   169→        """Check if session has been started and is active."""
   170→        return self._session_started and self._client_entered
   171→
   172→    def _get_project_context(self) -> str:
   173→        """Generate project context for the system prompt."""
   174→        lines = [f"Project: **{self.project_name}**"]
   175→        lines.append(f"Directory: `{self.project_dir}`")
   176→        lines.append("")
   177→
   178→        # Get current resources

    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}**"]

"""
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_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())

+    def get_message_history(self) -> list[dict]:

+        Get the message history for this session.

+            List of message dicts with role, content, timestamp

+        """Check if session has been started and is active."""

+        return self._session_started and self._client_entered


Now I need to modify the start() method to handle reconnection - if already started, just return the history instead of initializing a new greeting.


    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)

    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)