diff --git a/apps/api/src/api/terminal.py b/apps/api/src/api/terminal.py index 510e73e..7f11773 100644 --- a/apps/api/src/api/terminal.py +++ b/apps/api/src/api/terminal.py @@ -254,7 +254,6 @@ async def _handle_terminal_websocket( async def _read_loop(session_ref: SessionRef, websocket) -> None: """Read output from the container and send to WebSocket.""" - send_failures = 0 try: while True: session = session_ref.session @@ -265,15 +264,10 @@ async def _read_loop(session_ref: SessionRef, websocket) -> None: if data: try: await websocket.send_bytes(data) - send_failures = 0 + except WebSocketDisconnect: + break except Exception: - # Send failed — client may have disconnected. - # Allow a few retries before giving up so transient - # errors don't kill the whole session. - send_failures += 1 - if send_failures >= 3: - break - await asyncio.sleep(0.1) + break else: await asyncio.sleep(0.01) except Exception: diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index 5588328..ab2a82b 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -37,6 +37,7 @@ export interface TerminalProps { export interface TerminalRef { fit: () => void; + focus: () => void; } const FONT_SIZE_KEY = "terminal-font-size"; @@ -106,6 +107,7 @@ export const TerminalComponent = React.forwardRef( wsRef.current = ws; ws.onopen = () => { + console.log(`[Terminal ${sessionId ?? "default"}] WebSocket opened`); setStatus("connected"); setError(null); reconnectAttemptsRef.current = 0; @@ -137,6 +139,8 @@ export const TerminalComponent = React.forwardRef( if (!termRef.current) return; if (event.data instanceof Blob) { + // eslint-disable-next-line no-console + console.log(`[Terminal ${sessionId ?? "default"}] received ${(event.data as Blob).size} bytes`); event.data.arrayBuffer().then((buffer) => { const data = new Uint8Array(buffer); termRef.current?.write(data); @@ -306,6 +310,8 @@ export const TerminalComponent = React.forwardRef( // Open xterm first (must happen before fit) term.open(container); + term.focus(); + console.log(`[Terminal ${sessionId ?? "default"}] xterm opened and focused`); const ws = connectWebSocket(); // Initial fit after layout settles (terminal must be opened first) @@ -330,6 +336,8 @@ export const TerminalComponent = React.forwardRef( // Handle terminal input term.onData((data) => { + // eslint-disable-next-line no-console + console.log(`[Terminal ${sessionId ?? "default"}] sending:`, JSON.stringify(data)); const currentWs = wsRef.current; if (currentWs?.readyState !== WebSocket.OPEN) return; @@ -466,6 +474,9 @@ export const TerminalComponent = React.forwardRef( } } }, + focus: () => { + termRef.current?.focus(); + }, })); // Update parent about status changes diff --git a/apps/web/src/hooks/use-terminal-sessions.ts b/apps/web/src/hooks/use-terminal-sessions.ts index e05f527..a995b37 100644 --- a/apps/web/src/hooks/use-terminal-sessions.ts +++ b/apps/web/src/hooks/use-terminal-sessions.ts @@ -25,7 +25,7 @@ export function useTerminalSessions( ): UseTerminalSessionsResult { const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(null); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const loadSessions = useCallback(async () => { diff --git a/apps/web/src/pages/terminal.tsx b/apps/web/src/pages/terminal.tsx index 4fc6d4a..74ea37d 100644 --- a/apps/web/src/pages/terminal.tsx +++ b/apps/web/src/pages/terminal.tsx @@ -26,7 +26,6 @@ export const TerminalPage: React.FC = () => { const [isFullscreen, setIsFullscreen] = useState(false); const terminalRefs = useRef>>({}); const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile }); - const hasAutoCreated = useRef(false); const { sessions, @@ -40,16 +39,9 @@ export const TerminalPage: React.FC = () => { error, } = useTerminalSessions(instanceId ?? ""); - // Auto-create default session if none exist (guard against double-fire) + // Auto-create default session if none exist after loading completes useEffect(() => { - if ( - !loading && - sessions.length === 0 && - !error && - instanceId && - !hasAutoCreated.current - ) { - hasAutoCreated.current = true; + if (!loading && sessions.length === 0 && !error && instanceId) { void createSession("Session 1"); } }, [loading, sessions.length, error, instanceId, createSession]); @@ -70,13 +62,14 @@ export const TerminalPage: React.FC = () => { } }, [sessions]); - // Fit active terminal when switching tabs + // Fit and focus active terminal when switching tabs useEffect(() => { if (activeSessionId && terminalRefs.current[activeSessionId]) { const ref = terminalRefs.current[activeSessionId]; // Small delay to allow display:block to apply const timer = setTimeout(() => { ref.current?.fit(); + ref.current?.focus(); }, 50); return () => clearTimeout(timer); }