fix: terminal reconnect typing and reset functionality

Frontend:
- Fix term.onData to use wsRef.current instead of captured ws variable
- Fix fitTerminal to use wsRef.current for resize messages
- Fix sendData callback to use wsRef.current
- This fixes 'cannot type' after WebSocket reconnect

Backend:
- Add SessionRef class for mutable session reference
- Update _read_loop and _write_loop to use SessionRef
- Reset now updates session_ref.session instead of returning
- This keeps the WebSocket alive after reset instead of closing it
This commit is contained in:
Fusion
2026-05-24 20:57:33 +02:00
parent 0cb2eefd29
commit 33b482ce91
2 changed files with 39 additions and 17 deletions
+29 -10
View File
@@ -15,6 +15,13 @@ router = APIRouter()
logger = logging.getLogger(__name__)
class SessionRef:
"""Mutable reference to a terminal session, allowing updates during reset."""
def __init__(self, session):
self.session = session
@router.websocket(
"/ws/tool-instances/{instance_id}/terminal",
)
@@ -86,9 +93,12 @@ async def terminal_websocket(
# Send connected status
await websocket.send_json({"type": "status", "status": "connected"})
# Use mutable session reference so loops can survive reset
session_ref = SessionRef(session)
# Start I/O loops and heartbeat
read_task = asyncio.create_task(_read_loop(session, websocket))
write_task = asyncio.create_task(_write_loop(session, websocket, instance_id))
read_task = asyncio.create_task(_read_loop(session_ref, websocket))
write_task = asyncio.create_task(_write_loop(session_ref, websocket, instance_id))
heartbeat_task = asyncio.create_task(_heartbeat_loop(websocket))
# Wait for either task to complete (indicating disconnect or error)
@@ -114,10 +124,14 @@ async def terminal_websocket(
pass
async def _read_loop(session, websocket) -> None:
async def _read_loop(session_ref: SessionRef, websocket) -> None:
"""Read output from the container and send to WebSocket."""
try:
while session.is_alive() and not session._closed:
while True:
session = session_ref.session
if not session.is_alive() or session._closed:
await asyncio.sleep(0.1)
continue
data = await session.read_output()
if data:
try:
@@ -130,10 +144,14 @@ async def _read_loop(session, websocket) -> None:
pass
async def _write_loop(session, websocket, instance_id: str) -> None:
async def _write_loop(session_ref: SessionRef, websocket, instance_id: str) -> None:
"""Read input from WebSocket and send to container."""
try:
while session.is_alive() and not session._closed:
while True:
session = session_ref.session
if not session.is_alive() or session._closed:
await asyncio.sleep(0.1)
continue
message = await websocket.receive()
if message["type"] == "websocket.receive":
if "bytes" in message:
@@ -163,14 +181,15 @@ async def _write_loop(session, websocket, instance_id: str) -> None:
session.container_id,
)
# Update the mutable session reference so read_loop uses the new session
session_ref.session = new_session
# Attach to new session
await terminal_manager.attach_websocket(new_session, websocket)
await websocket.send_json({"type": "status", "status": "connected"})
# Update session reference and restart loops
# Note: This will cause the current loops to exit
# The WebSocket handler will create new ones
return
# Continue the loop with the new session
continue
except json.JSONDecodeError:
pass