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:
@@ -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:
|
||||
|
||||
@@ -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<TerminalRef, TerminalProps>(
|
||||
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<TerminalRef, TerminalProps>(
|
||||
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<TerminalRef, TerminalProps>(
|
||||
|
||||
// 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<TerminalRef, TerminalProps>(
|
||||
|
||||
// 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<TerminalRef, TerminalProps>(
|
||||
}
|
||||
}
|
||||
},
|
||||
focus: () => {
|
||||
termRef.current?.focus();
|
||||
},
|
||||
}));
|
||||
|
||||
// Update parent about status changes
|
||||
|
||||
@@ -25,7 +25,7 @@ export function useTerminalSessions(
|
||||
): UseTerminalSessionsResult {
|
||||
const [sessions, setSessions] = useState<TerminalSession[]>([]);
|
||||
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 loadSessions = useCallback(async () => {
|
||||
|
||||
@@ -26,7 +26,6 @@ export const TerminalPage: React.FC = () => {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const terminalRefs = useRef<Record<string, React.RefObject<TerminalRef>>>({});
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user