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:
|
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:
|
||||||
except Exception:
|
break
|
||||||
# Send failed — client may have disconnected.
|
except Exception:
|
||||||
# Allow a few retries before giving up so transient
|
|
||||||
# errors don't kill the whole session.
|
|
||||||
send_failures += 1
|
|
||||||
if send_failures >= 3:
|
|
||||||
break
|
break
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
else:
|
else:
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user