diff --git a/web/src/components/terminal/TerminalPanel.tsx b/web/src/components/terminal/TerminalPanel.tsx index 4d97167..43b103d 100644 --- a/web/src/components/terminal/TerminalPanel.tsx +++ b/web/src/components/terminal/TerminalPanel.tsx @@ -2,7 +2,7 @@ import { FitAddon } from "@xterm/addon-fit"; import { Terminal } from "@xterm/xterm"; import { useEffect, useRef, useState } from "react"; -import { useProcessWebSocket } from "@/lib/api/process"; +import { useProcessStatus, useProcessWebSocket } from "@/lib/api/process"; interface TerminalPanelProps { workspaceId: string | null; @@ -16,7 +16,12 @@ export function TerminalPanel({ workspaceId }: TerminalPanelProps) { const isDeadRef = useRef(false); const [failed, setFailed] = useState(false); const [isDead, setIsDead] = useState(false); - const { status, send, onData } = useProcessWebSocket(workspaceId); + const { data: processStatus } = useProcessStatus(workspaceId); + const processRunning = processStatus?.running === true; + const { status, send, onData } = useProcessWebSocket( + workspaceId, + processRunning, + ); // Initialize xterm once and keep it alive across WS status changes. useEffect(() => { @@ -148,11 +153,15 @@ export function TerminalPanel({ workspaceId }: TerminalPanelProps) { ? "connecting…" : null; + const hint = processRunning + ? statusHint + : "process not running — start it from the toolbar"; + return (
- {statusHint && ( + {hint && (
- {statusHint} + {hint}
)}
diff --git a/web/src/lib/api/process.ts b/web/src/lib/api/process.ts index 1d1b50c..863b44e 100644 --- a/web/src/lib/api/process.ts +++ b/web/src/lib/api/process.ts @@ -97,9 +97,10 @@ export interface ProcessWebSocket { export function useProcessWebSocket( workspaceId: string | null, + enabled: boolean = true, ): ProcessWebSocket { const [status, setStatus] = useState(() => - workspaceId ? "connecting" : "idle", + workspaceId && enabled ? "connecting" : "idle", ); const statusRef = useRef(status); useEffect(() => { @@ -133,14 +134,26 @@ export function useProcessWebSocket( }, [clearReconnectTimeout]); useEffect(() => { - if (!workspaceId) { + if (!workspaceId || !enabled) { setStatus("idle"); + clearReconnectTimeout(); + const socket = socketRef.current; + if (socket) { + socket.onclose = null; + socket.close(); + socketRef.current = null; + } + reconnectAttemptsRef.current = 0; + lastFrameWasExitRef.current = false; + intentionalCloseRef.current = false; + serverCleanCloseRef.current = false; return; } reconnectAttemptsRef.current = 0; lastFrameWasExitRef.current = false; intentionalCloseRef.current = false; + serverCleanCloseRef.current = false; const openSocket = () => { const wsUrl = @@ -211,9 +224,15 @@ export function useProcessWebSocket( openSocket(); return () => { - closeSocket(); + clearReconnectTimeout(); + const socket = socketRef.current; + if (socket) { + socket.onclose = null; + socket.close(); + socketRef.current = null; + } }; - }, [workspaceId, closeSocket]); + }, [workspaceId, enabled, clearReconnectTimeout]); const send = useCallback((data: string) => { const socket = socketRef.current;