import { useState, useEffect, useRef, useCallback } from "react"; import { X, Loader2, CheckCircle, XCircle, Terminal } from "lucide-react"; import { useTerminal } from "../hooks/useTerminal"; import { killPtyTerminalSession } from "../api"; import type { ConnectionStatus } from "../hooks/useTerminal"; interface ScriptRunDialogProps { isOpen: boolean; onClose: () => void; scriptName: string; sessionId: string; command: string; } type RunStatus = "running" | "completed" | "error"; /** * ScriptRunDialog — read-only modal that shows the output of a saved script run. * * Uses the existing PTY terminal session WebSocket plumbing to stream * live stdout/stderr output. The dialog is non-interactive (no input), * focused on observing the script execution and reporting exit status. * * When the dialog closes before the process exits, the backing PTY session * is killed to prevent orphaned sessions. */ export function ScriptRunDialog({ isOpen, onClose, scriptName, sessionId, command, }: ScriptRunDialogProps) { const [output, setOutput] = useState([]); const [exitCode, setExitCode] = useState(null); const [status, setStatus] = useState("running"); const outputEndRef = useRef(null); const isMountedRef = useRef(true); const hasKilledRef = useRef(false); // Connect to the PTY session via WebSocket const { connectionStatus, onData, onExit, onConnect, onScrollback } = useTerminal(isOpen ? sessionId : null); // Auto-scroll to bottom when new output arrives useEffect(() => { if (outputEndRef.current && typeof outputEndRef.current.scrollIntoView === "function") { outputEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [output]); // Subscribe to terminal data useEffect(() => { if (!isOpen) return; const unsubData = onData((data: string) => { if (isMountedRef.current) { setOutput((prev) => [...prev, data]); } }); const unsubScrollback = onScrollback((data: string) => { if (isMountedRef.current) { setOutput([data]); } }); const unsubExit = onExit((code: number) => { if (isMountedRef.current) { setExitCode(code); setStatus(code === 0 ? "completed" : "error"); } }); return () => { unsubData(); unsubScrollback(); unsubExit(); }; }, [isOpen, onData, onScrollback, onExit]); // Cleanup on unmount useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); // Kill the PTY session when dialog closes before the process exits const handleClose = useCallback(() => { if (status === "running" && sessionId && !hasKilledRef.current) { hasKilledRef.current = true; killPtyTerminalSession(sessionId).catch(() => { // Best-effort kill — ignore errors }); } onClose(); }, [status, sessionId, onClose]); // Reset state when dialog opens with a new session useEffect(() => { if (isOpen) { setOutput([]); setExitCode(null); setStatus("running"); hasKilledRef.current = false; } }, [isOpen, sessionId]); // Handle escape key useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { handleClose(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen, handleClose]); if (!isOpen) return null; const getStatusIcon = () => { switch (status) { case "running": return ; case "completed": return ; case "error": return ; } }; const getStatusText = () => { switch (status) { case "running": return connectionStatus === "connected" ? "Running..." : "Connecting..."; case "completed": return "Completed"; case "error": return `Failed (exit code ${exitCode})`; } }; return (
e.stopPropagation()} role="dialog" aria-label={`Running script: ${scriptName}`} > {/* Header */}

{scriptName}

{/* Command */}
Command: {command}
{/* Output area */}
{output.length === 0 && status === "running" ? (
Waiting for output...
) : (
              {output.join("")}
              
)}
{/* Status bar */}
{getStatusIcon()} {getStatusText()}
{exitCode !== null && ( Exit code: {exitCode} )}
); }