From ab421e318497e263ddaf72fa5527da5c0251b709 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sat, 6 Dec 2025 23:06:18 +0100 Subject: [PATCH] Update AppImage --- AppImage/components/script-terminal-modal.tsx | 348 ++++++++++++++++-- AppImage/components/terminal-panel.tsx | 21 +- 2 files changed, 320 insertions(+), 49 deletions(-) diff --git a/AppImage/components/script-terminal-modal.tsx b/AppImage/components/script-terminal-modal.tsx index 8a0a6be..e28890f 100644 --- a/AppImage/components/script-terminal-modal.tsx +++ b/AppImage/components/script-terminal-modal.tsx @@ -1,13 +1,13 @@ "use client" import type React from "react" -import { useState, useRef } from "react" +import { useState, useEffect, useRef, useCallback } from "react" import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { CheckCircle2, XCircle, GripHorizontal } from "lucide-react" -import { TerminalPanel } from "./terminal-panel" +import { CheckCircle2, XCircle, Loader2, Activity, GripHorizontal } from "lucide-react" +import { API_PORT } from "../lib/api-config" import { useIsMobile } from "@/hooks/use-mobile" interface WebInteraction { @@ -38,36 +38,312 @@ export function ScriptTerminalModal({ title, description, }: ScriptTerminalModalProps) { + const termRef = useRef(null) + const wsRef = useRef(null) + const fitAddonRef = useRef(null) + const sessionIdRef = useRef(Math.random().toString(36).substring(2, 8)) + + const [isConnected, setIsConnected] = useState(false) const [isComplete, setIsComplete] = useState(false) const [exitCode, setExitCode] = useState(null) const [currentInteraction, setCurrentInteraction] = useState(null) const [interactionInput, setInteractionInput] = useState("") + const checkConnectionInterval = useRef(null) const isMobile = useIsMobile() + const [isWaitingNextInteraction, setIsWaitingNextInteraction] = useState(false) + const waitingTimeoutRef = useRef(null) + const [modalHeight, setModalHeight] = useState(80) const [isResizing, setIsResizing] = useState(false) const startYRef = useRef(0) const startHeightRef = useRef(80) - const initMessage = { - script_path: scriptPath, - params: { - EXECUTION_MODE: "web", - ...params, + const terminalContainerRef = useCallback( + (node: HTMLDivElement | null) => { + if (!node || !open || termRef.current) { + return + } + + console.log("[v0] Terminal container mounted, initializing...") + + const initializeTerminal = async () => { + console.log("[v0] Loading xterm modules...") + const [TerminalClass, FitAddonClass] = await Promise.all([ + import("xterm").then((mod) => mod.Terminal), + import("xterm-addon-fit").then((mod) => mod.FitAddon), + import("xterm/css/xterm.css"), + ]) + + console.log("[v0] Creating terminal instance...") + const fontSize = window.innerWidth < 768 ? 12 : 16 + + const term = new TerminalClass({ + rendererType: "dom", + fontFamily: '"Courier", "Courier New", "Liberation Mono", "DejaVu Sans Mono", monospace', + fontSize: fontSize, + lineHeight: 1, + cursorBlink: true, + scrollback: 2000, + disableStdin: false, + customGlyphs: true, + fontWeight: "500", + fontWeightBold: "700", + theme: { + background: "#000000", + foreground: "#ffffff", + cursor: "#ffffff", + cursorAccent: "#000000", + black: "#2e3436", + red: "#cc0000", + green: "#4e9a06", + yellow: "#c4a000", + blue: "#3465a4", + magenta: "#75507b", + cyan: "#06989a", + white: "#d3d7cf", + brightBlack: "#555753", + brightRed: "#ef2929", + brightGreen: "#8ae234", + brightYellow: "#fce94f", + brightBlue: "#729fcf", + brightMagenta: "#ad7fa8", + brightCyan: "#34e2e2", + brightWhite: "#eeeeec", + }, + }) + + const fitAddon = new FitAddonClass() + term.loadAddon(fitAddon) + console.log("[v0] Opening terminal in container...") + term.open(node) + + termRef.current = term + fitAddonRef.current = fitAddon + + setTimeout(() => { + try { + fitAddon.fit() + console.log("[v0] Terminal fitted, cols:", term.cols, "rows:", term.rows) + } catch (err) { + console.log("[v0] Fit error:", err) + } + }, 50) + + const wsUrl = getScriptWebSocketUrl(sessionIdRef.current) + console.log("[v0] Connecting to WebSocket:", wsUrl) + const ws = new WebSocket(wsUrl) + wsRef.current = ws + + ws.onopen = () => { + console.log("[v0] WebSocket connected!") + setIsConnected(true) + + const initMessage = { + script_path: scriptPath, + params: { + EXECUTION_MODE: "web", + ...params, + }, + } + + console.log("[v0] Sending init message:", initMessage) + ws.send(JSON.stringify(initMessage)) + + setTimeout(() => { + try { + fitAddon.fit() + const cols = term.cols + const rows = term.rows + console.log("[v0] Sending resize:", { cols, rows }) + ws.send( + JSON.stringify({ + type: "resize", + cols: cols, + rows: rows, + }), + ) + } catch (err) { + console.log("[v0] Resize error:", err) + } + }, 100) + } + + ws.onmessage = (event) => { + console.log("[v0] WebSocket message received:", event.data.substring(0, 100)) + try { + const msg = JSON.parse(event.data) + + if (msg.type === "web_interaction" && msg.interaction) { + console.log("[v0] Web interaction detected:", msg.interaction.type) + setIsWaitingNextInteraction(false) + if (waitingTimeoutRef.current) { + clearTimeout(waitingTimeoutRef.current) + } + setCurrentInteraction({ + type: msg.interaction.type, + id: msg.interaction.id, + title: msg.interaction.title || "", + message: msg.interaction.message || "", + options: msg.interaction.options, + default: msg.interaction.default, + }) + return + } + + if (msg.type === "error") { + console.log("[v0] Error message:", msg.message) + term.writeln(`\x1b[31m${msg.message}\x1b[0m`) + return + } + } catch { + // Not JSON, es output normal de terminal + } + + term.write(event.data) + + setIsWaitingNextInteraction(false) + if (waitingTimeoutRef.current) { + clearTimeout(waitingTimeoutRef.current) + } + } + + ws.onerror = (error) => { + console.log("[v0] WebSocket error:", error) + setIsConnected(false) + term.writeln("\x1b[31mWebSocket error occurred\x1b[0m") + } + + ws.onclose = (event) => { + console.log("[v0] WebSocket closed:", event.code, event.reason) + setIsConnected(false) + term.writeln("\x1b[33mConnection closed\x1b[0m") + + if (!isComplete) { + setIsComplete(true) + setExitCode(event.code === 1000 ? 0 : 1) + } + } + + term.onData((data) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data) + } + }) + + checkConnectionInterval.current = setInterval(() => { + if (ws) { + setIsConnected(ws.readyState === WebSocket.OPEN) + } + }, 500) + + let resizeTimeout: NodeJS.Timeout | null = null + + const resizeObserver = new ResizeObserver(() => { + if (resizeTimeout) clearTimeout(resizeTimeout) + resizeTimeout = setTimeout(() => { + if (fitAddon && term && ws?.readyState === WebSocket.OPEN) { + try { + fitAddon.fit() + ws.send( + JSON.stringify({ + type: "resize", + cols: term.cols, + rows: term.rows, + }), + ) + } catch (err) { + // Ignore + } + } + }, 100) + }) + + resizeObserver.observe(node) + } + + initializeTerminal() }, + [open, scriptPath, params], + ) + + useEffect(() => { + if (!open) { + if (checkConnectionInterval.current) { + clearInterval(checkConnectionInterval.current) + } + if (waitingTimeoutRef.current) { + clearTimeout(waitingTimeoutRef.current) + } + if (wsRef.current) { + wsRef.current.close() + wsRef.current = null + } + if (termRef.current) { + termRef.current.dispose() + termRef.current = null + } + sessionIdRef.current = Math.random().toString(36).substring(2, 8) + setIsComplete(false) + setExitCode(null) + setInteractionInput("") + setCurrentInteraction(null) + setIsWaitingNextInteraction(false) + setIsConnected(false) + } + }, [open]) + + const getScriptWebSocketUrl = (sid: string): string => { + if (typeof window === "undefined") { + return `ws://localhost:${API_PORT}/ws/script/${sid}` + } + + const { hostname, protocol } = window.location + const wsProtocol = protocol === "https:" ? "wss:" : "ws:" + return `${wsProtocol}//${hostname}:${API_PORT}/ws/script/${sid}` } const handleInteractionResponse = (value: string) => { - if (value === "cancel" || value === "") { - setCurrentInteraction(null) - setInteractionInput("") - onClose() + if (!wsRef.current || !currentInteraction) { return } - // TerminalPanel manejará el envío de la respuesta + if (value === "cancel" || value === "") { + setCurrentInteraction(null) + setInteractionInput("") + handleCloseModal() + return + } + + const response = JSON.stringify({ + type: "interaction_response", + id: currentInteraction.id, + value: value, + }) + + if (wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.send(response) + } + setCurrentInteraction(null) setInteractionInput("") + + waitingTimeoutRef.current = setTimeout(() => { + setIsWaitingNextInteraction(true) + }, 300) + } + + const handleCloseModal = () => { + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.close() + } + if (checkConnectionInterval.current) { + clearInterval(checkConnectionInterval.current) + } + if (termRef.current) { + termRef.current.dispose() + } + onClose() } const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => { @@ -120,22 +396,17 @@ export function ScriptTerminalModal({ -
- { - setCurrentInteraction({ - type: interaction.type as any, - id: interaction.id, - title: interaction.title || "", - message: interaction.message || "", - options: interaction.options, - default: interaction.default, - }) - }} - onClose={onClose} - /> +
+
+ + {isWaitingNextInteraction && !currentInteraction && ( +
+
+ +

Processing...

+
+
+ )}
{!isMobile && ( @@ -149,6 +420,25 @@ export function ScriptTerminalModal({
)} + +
+
+ +
+ {isConnected ? "Online" : "Offline"} +
+ + +
diff --git a/AppImage/components/terminal-panel.tsx b/AppImage/components/terminal-panel.tsx index e743428..d03ec7b 100644 --- a/AppImage/components/terminal-panel.tsx +++ b/AppImage/components/terminal-panel.tsx @@ -26,9 +26,6 @@ import type { CheatSheetResult } from "@/lib/cheat-sheet-result" // Declare Chea type TerminalPanelProps = { websocketUrl?: string onClose?: () => void - isScriptModal?: boolean - initMessage?: { script_path: string; params: Record } - onWebInteraction?: (interaction: any) => void } interface TerminalInstance { @@ -135,13 +132,7 @@ const proxmoxCommands = [ { cmd: "clear", desc: "Clear terminal screen" }, ] -export const TerminalPanel: React.FC = ({ - websocketUrl, - onClose, - isScriptModal, - initMessage, - onWebInteraction, -}) => { +export const TerminalPanel: React.FC = ({ websocketUrl, onClose }) => { const [terminals, setTerminals] = useState([]) const [activeTerminalId, setActiveTerminalId] = useState("") const [layout, setLayout] = useState<"single" | "grid">("grid") @@ -579,16 +570,6 @@ export const TerminalPanel: React.FC = ({ const activeTerminal = terminals.find((t) => t.id === activeTerminalId) - useEffect(() => { - if (initMessage && activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) { - const message = JSON.stringify(initMessage) - activeTerminal.ws.send(message) - if (onWebInteraction) { - onWebInteraction({ type: "script_init", message }) - } - } - }, [initMessage, activeTerminal]) - return (