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:
@@ -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
|
||||
|
||||
@@ -225,8 +225,9 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
if (cols !== oldCols || rows !== oldRows) {
|
||||
termRef.current.refresh(0, rows - 1);
|
||||
}
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: "resize", cols, rows }));
|
||||
const currentWs = wsRef.current;
|
||||
if (currentWs?.readyState === WebSocket.OPEN) {
|
||||
currentWs.send(JSON.stringify({ type: "resize", cols, rows }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -244,20 +245,21 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
|
||||
// Handle terminal input
|
||||
term.onData((data) => {
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
const currentWs = wsRef.current;
|
||||
if (currentWs?.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
// Apply active modifier to single-character input
|
||||
const modifier = activeModifierRef.current;
|
||||
if (modifier && data.length === 1) {
|
||||
const modified = applyModifierToChar(data, modifier);
|
||||
if (modified) {
|
||||
ws.send(modified);
|
||||
currentWs.send(modified);
|
||||
onModifierChange?.(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ws.send(data);
|
||||
currentWs.send(data);
|
||||
});
|
||||
|
||||
// Handle container resize with ResizeObserver for accurate dimension tracking
|
||||
@@ -302,8 +304,9 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
// Notify parent about terminal readiness
|
||||
if (onTerminalReadyRef.current) {
|
||||
const sendData = (data: string) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(data);
|
||||
const currentWs = wsRef.current;
|
||||
if (currentWs?.readyState === WebSocket.OPEN) {
|
||||
currentWs.send(data);
|
||||
}
|
||||
};
|
||||
const focusInput = () => {
|
||||
|
||||
Reference in New Issue
Block a user