fix(terminal): loading state, focus handling, debug logging

- Initialize loading=true in useTerminalSessions to prevent auto-create
  from firing before initial load completes
- Remove hasAutoCreated ref from TerminalPage (no longer needed)
- Add focus() to TerminalRef, call on tab switch
- Add term.focus() after term.open() in TerminalComponent
- Add console logging for WebSocket send/receive to debug no-i/o
- Revert backend _read_loop retry logic to original break-on-error
This commit is contained in:
2026-05-28 20:14:54 +02:00
parent b6e71e32f5
commit a3d01dd0a5
4 changed files with 19 additions and 21 deletions
+3 -9
View File
@@ -254,7 +254,6 @@ async def _handle_terminal_websocket(
async def _read_loop(session_ref: SessionRef, websocket) -> None: async def _read_loop(session_ref: SessionRef, websocket) -> None:
"""Read output from the container and send to WebSocket.""" """Read output from the container and send to WebSocket."""
send_failures = 0
try: try:
while True: while True:
session = session_ref.session session = session_ref.session
@@ -265,15 +264,10 @@ async def _read_loop(session_ref: SessionRef, websocket) -> None:
if data: if data:
try: try:
await websocket.send_bytes(data) await websocket.send_bytes(data)
send_failures = 0 except WebSocketDisconnect:
break
except Exception: except Exception:
# Send failed — client may have disconnected. break
# 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)
else: else:
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
except Exception: except Exception:
+11
View File
@@ -37,6 +37,7 @@ export interface TerminalProps {
export interface TerminalRef { export interface TerminalRef {
fit: () => void; fit: () => void;
focus: () => void;
} }
const FONT_SIZE_KEY = "terminal-font-size"; const FONT_SIZE_KEY = "terminal-font-size";
@@ -106,6 +107,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
wsRef.current = ws; wsRef.current = ws;
ws.onopen = () => { ws.onopen = () => {
console.log(`[Terminal ${sessionId ?? "default"}] WebSocket opened`);
setStatus("connected"); setStatus("connected");
setError(null); setError(null);
reconnectAttemptsRef.current = 0; reconnectAttemptsRef.current = 0;
@@ -137,6 +139,8 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
if (!termRef.current) return; if (!termRef.current) return;
if (event.data instanceof Blob) { 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) => { event.data.arrayBuffer().then((buffer) => {
const data = new Uint8Array(buffer); const data = new Uint8Array(buffer);
termRef.current?.write(data); termRef.current?.write(data);
@@ -306,6 +310,8 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
// Open xterm first (must happen before fit) // Open xterm first (must happen before fit)
term.open(container); term.open(container);
term.focus();
console.log(`[Terminal ${sessionId ?? "default"}] xterm opened and focused`);
const ws = connectWebSocket(); const ws = connectWebSocket();
// Initial fit after layout settles (terminal must be opened first) // Initial fit after layout settles (terminal must be opened first)
@@ -330,6 +336,8 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
// Handle terminal input // Handle terminal input
term.onData((data) => { term.onData((data) => {
// eslint-disable-next-line no-console
console.log(`[Terminal ${sessionId ?? "default"}] sending:`, JSON.stringify(data));
const currentWs = wsRef.current; const currentWs = wsRef.current;
if (currentWs?.readyState !== WebSocket.OPEN) return; if (currentWs?.readyState !== WebSocket.OPEN) return;
@@ -466,6 +474,9 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
} }
} }
}, },
focus: () => {
termRef.current?.focus();
},
})); }));
// Update parent about status changes // Update parent about status changes
+1 -1
View File
@@ -25,7 +25,7 @@ export function useTerminalSessions(
): UseTerminalSessionsResult { ): UseTerminalSessionsResult {
const [sessions, setSessions] = useState<TerminalSession[]>([]); const [sessions, setSessions] = useState<TerminalSession[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string | null>(null); const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const loadSessions = useCallback(async () => { const loadSessions = useCallback(async () => {
+4 -11
View File
@@ -26,7 +26,6 @@ export const TerminalPage: React.FC = () => {
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const terminalRefs = useRef<Record<string, React.RefObject<TerminalRef>>>({}); const terminalRefs = useRef<Record<string, React.RefObject<TerminalRef>>>({});
const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile }); const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
const hasAutoCreated = useRef(false);
const { const {
sessions, sessions,
@@ -40,16 +39,9 @@ export const TerminalPage: React.FC = () => {
error, error,
} = useTerminalSessions(instanceId ?? ""); } = useTerminalSessions(instanceId ?? "");
// Auto-create default session if none exist (guard against double-fire) // Auto-create default session if none exist after loading completes
useEffect(() => { useEffect(() => {
if ( if (!loading && sessions.length === 0 && !error && instanceId) {
!loading &&
sessions.length === 0 &&
!error &&
instanceId &&
!hasAutoCreated.current
) {
hasAutoCreated.current = true;
void createSession("Session 1"); void createSession("Session 1");
} }
}, [loading, sessions.length, error, instanceId, createSession]); }, [loading, sessions.length, error, instanceId, createSession]);
@@ -70,13 +62,14 @@ export const TerminalPage: React.FC = () => {
} }
}, [sessions]); }, [sessions]);
// Fit active terminal when switching tabs // Fit and focus active terminal when switching tabs
useEffect(() => { useEffect(() => {
if (activeSessionId && terminalRefs.current[activeSessionId]) { if (activeSessionId && terminalRefs.current[activeSessionId]) {
const ref = terminalRefs.current[activeSessionId]; const ref = terminalRefs.current[activeSessionId];
// Small delay to allow display:block to apply // Small delay to allow display:block to apply
const timer = setTimeout(() => { const timer = setTimeout(() => {
ref.current?.fit(); ref.current?.fit();
ref.current?.focus();
}, 50); }, 50);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }