"use client" import type React from "react" import { useEffect, useRef, useState } from "react" import { API_PORT, fetchApi } from "@/lib/api-config" // Unificando importaciones de api-config en una sola línea con alias @/ import { getTicketedWsUrl } from "@/lib/terminal-ws" import { Activity, Trash2, X, Search, Send, Lightbulb, Terminal, Plus, AlignJustify, Grid2X2, GripHorizontal, ChevronDown, Copy, Clipboard, } from "lucide-react" import { copyTerminalSelection, pasteFromClipboard } from "@/lib/terminal-clipboard" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuLabel, } from "@/components/ui/dropdown-menu" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" import type { CheatSheetResult } from "@/lib/cheat-sheet-result" // Declare CheatSheetResult here import { useT } from "@/lib/i18n/provider" type TerminalPanelProps = { websocketUrl?: string onClose?: () => void } interface TerminalInstance { id: string title: string term: any ws: WebSocket | null isConnected: boolean fitAddon: any // Added fitAddon to TerminalInstance pingInterval?: ReturnType | null // Heartbeat interval to keep connection alive } function getWebSocketUrl(): string { if (typeof window === "undefined") { return "ws://localhost:8008/ws/terminal" } const { protocol, hostname, port } = window.location const isStandardPort = port === "" || port === "80" || port === "443" const wsProtocol = protocol === "https:" ? "wss:" : "ws:" if (isStandardPort) { return `${wsProtocol}//${hostname}/ws/terminal` } else { return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal` } } function getApiUrl(endpoint?: string): string { if (typeof window === "undefined") { return "http://localhost:8008" } const { protocol, hostname } = window.location const apiProtocol = protocol === "https:" ? "https:" : "http:" return `${apiProtocol}//${hostname}:${API_PORT}${endpoint || ""}` } const proxmoxCommands = [ "pvesh get /nodes", "pvesh get /nodes/{node}/qemu", "pvesh get /nodes/{node}/lxc", "pvesh get /nodes/{node}/storage", "pvesh get /nodes/{node}/network", "qm list", "qm start ", "qm stop ", "qm shutdown ", "qm status ", "qm config ", "qm snapshot ", "pct list", "pct start ", "pct stop ", "pct enter ", "pct config ", "pvesm status", "pvesm list ", "pveperf", "pveversion", "systemctl status pve-cluster", "pvecm status", "pvecm nodes", "zpool status", "zpool list", "zfs list", "ls -la", "cd /path/to/dir", "mkdir dirname", "rm -rf dirname", "cp source dest", "mv source dest", "cat filename", "grep 'pattern' file", "find . -name 'file'", "chmod 755 file", "chown user:group file", "tar -xzf file.tar.gz", "tar -czf archive.tar.gz dir/", "df -h", "du -sh *", "free -h", "top", "ps aux | grep process", "kill -9 PID", "systemctl status service", "systemctl start service", "systemctl stop service", "systemctl restart service", "apt update && apt upgrade", "apt install package", "apt remove package", "docker ps", "docker images", "docker exec -it container bash", "ip addr show", "ping host", "curl -I url", "wget url", "ssh user@host", "scp file user@host:/path", "tail -f /var/log/syslog", "history", "clear", ] export const TerminalPanel: React.FC = ({ websocketUrl, onClose }) => { const t = useT() const localizedCommands = proxmoxCommands.map((cmd, index) => ({ cmd, desc: t(`terminal.commandDescriptions.${index}`), })) const [terminals, setTerminals] = useState([]) const [activeTerminalId, setActiveTerminalId] = useState("") const [layout, setLayout] = useState<"single" | "grid">("grid") const [isMobile, setIsMobile] = useState(false) const [isTablet, setIsTablet] = useState(false) const [terminalHeight, setTerminalHeight] = useState(500) // altura por defecto en px const [searchModalOpen, setSearchModalOpen] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [filteredCommands, setFilteredCommands] = useState>(localizedCommands) const [isSearching, setIsSearching] = useState(false) const [searchResults, setSearchResults] = useState([]) const [useOnline, setUseOnline] = useState(true) const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}) // Per-terminal reconnect attempt count + last-fired timestamp for the // exponential backoff in the visibilitychange handler. const reconnectAttemptsRef = useRef<{ [key: string]: { attempts: number; lastAt: number } }>({}) useEffect(() => { const updateDeviceType = () => { const width = window.innerWidth const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0 const isTabletSize = width >= 768 && width <= 1366 // iPads Pro pueden llegar a 1366px setIsMobile(width < 768) setIsTablet(isTouchDevice && isTabletSize) } updateDeviceType() const handleResize = () => updateDeviceType() window.addEventListener("resize", handleResize) const savedHeight = localStorage.getItem("terminalHeight") if (savedHeight) { setTerminalHeight(Number.parseInt(savedHeight, 10)) } return () => { window.removeEventListener("resize", handleResize) } }, []) // Handle page visibility change for automatic reconnection when user returns // This is especially important for mobile/tablet devices (iPad) where switching apps // puts the browser tab in background and may close WebSocket connections // // Per-terminal exponential backoff (2s, 4s, 8s, ..., capped at 60s) so a // server-side outage doesn't get hammered every time the user switches // tabs. `reconnectAttemptsRef` survives re-renders and tracks attempts + // last-fired timestamps. The success path in `reconnectTerminal.onopen` // resets the counter back to 0. useEffect(() => { const handleVisibilityChange = () => { if (document.visibilityState !== 'visible') return const now = Date.now() terminals.forEach((terminal) => { if (!(terminal.ws && terminal.ws.readyState !== WebSocket.OPEN && terminal.term)) { return } const state = reconnectAttemptsRef.current[terminal.id] || { attempts: 0, lastAt: 0 } const backoffMs = Math.min(60000, 2000 * Math.pow(2, state.attempts)) if (now - state.lastAt < backoffMs) { return } reconnectAttemptsRef.current[terminal.id] = { attempts: state.attempts + 1, lastAt: now, } reconnectTerminal(terminal.id) }) } document.addEventListener('visibilitychange', handleVisibilityChange) return () => { document.removeEventListener('visibilitychange', handleVisibilityChange) } }, [terminals]) const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => { // Bloquear solo en pantallas muy pequeñas (móviles) if (window.innerWidth < 640 && !isTablet) { return } e.preventDefault() e.stopPropagation() // Detectar si es touch o mouse const clientY = "touches" in e ? e.touches[0].clientY : e.clientY const startY = clientY const startHeight = terminalHeight const handleMove = (moveEvent: MouseEvent | TouchEvent) => { const currentY = "touches" in moveEvent ? moveEvent.touches[0].clientY : moveEvent.clientY const deltaY = currentY - startY const newHeight = Math.max(200, Math.min(2400, startHeight + deltaY)) setTerminalHeight(newHeight) } const handleEnd = () => { document.removeEventListener("mousemove", handleMove as any) document.removeEventListener("mouseup", handleEnd) document.removeEventListener("touchmove", handleMove as any) document.removeEventListener("touchend", handleEnd) localStorage.setItem("terminalHeight", terminalHeight.toString()) } document.addEventListener("mousemove", handleMove as any) document.addEventListener("mouseup", handleEnd) document.addEventListener("touchmove", handleMove as any, { passive: false }) document.addEventListener("touchend", handleEnd) } useEffect(() => { if (terminals.length === 0) { addNewTerminal() } }, []) useEffect(() => { const searchCheatSh = async (query: string) => { if (!query.trim()) { setSearchResults([]) setFilteredCommands(localizedCommands) return } try { setIsSearching(true) const searchEndpoint = `/api/terminal/search-command?q=${encodeURIComponent(query)}` const data = await fetchApi<{ success: boolean; examples: any[] }>(searchEndpoint, { method: "GET", signal: AbortSignal.timeout(10000), }) if (!data.success || !data.examples || data.examples.length === 0) { throw new Error(t("terminal.noExamplesFound")) } const formattedResults: CheatSheetResult[] = data.examples.map((example: any) => ({ command: example.command, description: example.description || "", examples: [example.command], })) setUseOnline(true) setSearchResults(formattedResults) } catch (error) { const filtered = localizedCommands.filter( (item) => item.cmd.toLowerCase().includes(query.toLowerCase()) || item.desc.toLowerCase().includes(query.toLowerCase()), ) setFilteredCommands(filtered) setSearchResults([]) setUseOnline(false) } finally { setIsSearching(false) } } const debounce = setTimeout(() => { if (searchQuery && searchQuery.length >= 2) { searchCheatSh(searchQuery) } else { setSearchResults([]) setFilteredCommands(localizedCommands) } }, 800) return () => clearTimeout(debounce) }, [searchQuery, t]) // Function to reconnect a terminal when connection is lost // This is called when page visibility changes (user returns from another app) const reconnectTerminal = async (terminalId: string) => { const terminal = terminals.find(t => t.id === terminalId) if (!terminal || !terminal.term) return // Show reconnecting message terminal.term.writeln(`\r\n\x1b[33m[INFO] ${t("terminal.reconnecting")}\x1b[0m`) const wsUrl = websocketUrl || getWebSocketUrl() // Append the single-use auth ticket so the backend handshake can validate. const ws = new WebSocket(await getTicketedWsUrl(wsUrl)) ws.onopen = () => { // Successful connect — reset backoff state for this terminal. reconnectAttemptsRef.current[terminalId] = { attempts: 0, lastAt: 0 } // Clear any existing ping interval if (terminal.pingInterval) { clearInterval(terminal.pingInterval) } // Start heartbeat ping every 25 seconds to keep connection alive const pingInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })) } else { clearInterval(pingInterval) } }, 25000) setTerminals((prev) => prev.map((t) => (t.id === terminalId ? { ...t, isConnected: true, ws, pingInterval } : t)) ) terminal.term.writeln(`\r\n\x1b[32m[INFO] ${t("terminal.reconnected")}\x1b[0m`) // Sync terminal size if (terminal.fitAddon) { try { terminal.fitAddon.fit() ws.send(JSON.stringify({ type: 'resize', cols: terminal.term.cols, rows: terminal.term.rows, })) } catch (err) { console.warn('[Terminal] resize on reconnect failed:', err) } } } ws.onmessage = (event) => { // Filter out pong responses from heartbeat - don't display in terminal if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') { return } terminal.term.write(event.data) } ws.onerror = () => { terminal.term.writeln(`\r\n\x1b[31m[ERROR] ${t("terminal.reconnectionFailed")}\x1b[0m`) } ws.onclose = () => { setTerminals((prev) => prev.map((t) => { if (t.id === terminalId) { if (t.pingInterval) { clearInterval(t.pingInterval) } return { ...t, isConnected: false, pingInterval: null } } return t })) terminal.term.writeln(`\r\n\x1b[33m[INFO] ${t("terminal.connectionClosed")}\x1b[0m`) } terminal.term.onData((data: string) => { if (ws.readyState === WebSocket.OPEN) { ws.send(data) } }) } const addNewTerminal = () => { if (terminals.length >= 4) return const newId = `terminal-${Date.now()}` setTerminals((prev) => [ ...prev, { id: newId, title: t("terminal.terminalTitle", { number: prev.length + 1 }), term: null, ws: null, isConnected: false, fitAddon: null, // Added fitAddon initialization pingInterval: null, // Added pingInterval initialization }, ]) setActiveTerminalId(newId) } const closeTerminal = (id: string) => { const terminal = terminals.find((t) => t.id === id) if (terminal) { // Clear heartbeat interval if (terminal.pingInterval) { clearInterval(terminal.pingInterval) } if (terminal.ws) { terminal.ws.close() } if (terminal.term) { terminal.term.dispose() } } setTerminals((prev) => { const filtered = prev.filter((t) => t.id !== id) if (filtered.length > 0 && activeTerminalId === id) { setActiveTerminalId(filtered[0].id) } return filtered }) delete containerRefs.current[id] } useEffect(() => { // Small delay to ensure DOM refs are available after state update // This fixes the issue where first terminal doesn't connect on mobile/VPN const timer = setTimeout(() => { terminals.forEach((terminal) => { const container = containerRefs.current[terminal.id] if (!terminal.term && container) { initializeTerminal(terminal, container) } }) }, 50) return () => clearTimeout(timer) }, [terminals, isMobile]) useEffect(() => { if (window.innerWidth < 640) return terminals.forEach((terminal) => { if (terminal.term && terminal.fitAddon && terminal.isConnected) { try { setTimeout(() => { terminal.fitAddon?.fit() if (terminal.ws?.readyState === WebSocket.OPEN) { const cols = terminal.term?.cols || 80 const rows = terminal.term?.rows || 24 terminal.ws.send( JSON.stringify({ type: "resize", cols, rows, }), ) } }, 100) } catch (err) { console.warn("[Terminal] resize on height change failed:", err) } } }) }, [terminalHeight, layout, terminals, isMobile]) const initializeTerminal = async (terminal: TerminalInstance, container: HTMLDivElement) => { 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"), ]).then(([Terminal, FitAddon]) => [Terminal, FitAddon]) // After the (potentially slow) dynamic import, verify the container // is still the one we were given. If the user removed the terminal // tab while xterm was loading, the original `container` element is // detached and `containerRefs.current[terminal.id]` is gone — bail // out to avoid attaching to a stale DOM node + opening an orphan // WebSocket. Audit Tier 6 — `import("xterm")` sin cancelación. if (containerRefs.current[terminal.id] !== container) return const fontSize = window.innerWidth < 768 ? 12 : 16 const term = new TerminalClass({ rendererType: "dom", // Issue #182: prepend common Nerd Font families so users who already // have one installed see Starship/atuin/ble.sh icons render. Falls // back to Courier if no NF is present. fontFamily: '"MesloLGS NF", "FiraCode Nerd Font", "JetBrainsMono Nerd Font", "Hack Nerd Font", "Symbols Nerd Font", "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) term.open(container) fitAddon.fit() const wsUrl = websocketUrl || getWebSocketUrl() // Connection with timeout for VPN/mobile (15 seconds) const connectionTimeout = 15000 let connectionTimedOut = false // Single-use auth ticket appended as ?ticket=... — see lib/terminal-ws.ts. const ws = new WebSocket(await getTicketedWsUrl(wsUrl)) // Set connection timeout const timeoutId = setTimeout(() => { if (ws.readyState !== WebSocket.OPEN) { connectionTimedOut = true ws.close() term.writeln(`\x1b[31m[ERROR] ${t("terminal.connectionTimeout")}\x1b[0m`) term.writeln(`\x1b[33m[TIP] ${t("terminal.vpnTip")}\x1b[0m`) } }, connectionTimeout) const syncSizeWithBackend = () => { try { fitAddon.fit() if (ws.readyState === WebSocket.OPEN) { const cols = term.cols const rows = term.rows ws.send( JSON.stringify({ type: "resize", cols, rows, }), ) } } catch (err) { console.warn("[Terminal] resize failed:", err) } } ws.onopen = () => { // Clear connection timeout - we're connected! clearTimeout(timeoutId) // Start heartbeat ping every 25 seconds to keep connection alive // This prevents disconnection when switching apps on mobile/tablet (iPad) const pingInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })) } else { clearInterval(pingInterval) } }, 25000) setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws, fitAddon, pingInterval } : t)), ) syncSizeWithBackend() } ws.onmessage = (event) => { // Filter out pong responses from heartbeat - don't display in terminal if (event.data === '{"type": "pong"}' || event.data === '{"type":"pong"}') { return } term.write(event.data) } ws.onerror = (error) => { clearTimeout(timeoutId) console.error("TerminalPanel: WebSocket error:", error) setTerminals((prev) => prev.map((t) => { if (t.id === terminal.id) { if (t.pingInterval) { clearInterval(t.pingInterval) } return { ...t, isConnected: false, pingInterval: null } } return t })) // Only show error if not already shown by timeout if (!connectionTimedOut) { term.writeln(`\r\n\x1b[31m[ERROR] ${t("terminal.websocketError")}\x1b[0m`) } } ws.onclose = () => { clearTimeout(timeoutId) setTerminals((prev) => prev.map((t) => { if (t.id === terminal.id) { if (t.pingInterval) { clearInterval(t.pingInterval) } return { ...t, isConnected: false, pingInterval: null } } return t })) // Only show close message if not already shown by timeout if (!connectionTimedOut) { term.writeln(`\r\n\x1b[33m[INFO] ${t("terminal.connectionClosed")}\x1b[0m`) } } term.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(data) } }) let resizeTimeout: any = null const handleResize = () => { clearTimeout(resizeTimeout) resizeTimeout = setTimeout(() => { syncSizeWithBackend() }, 150) } window.addEventListener("resize", handleResize) return () => { window.removeEventListener("resize", handleResize) ws.close() term.dispose() } } const handleKeyButton = (key: string, e?: React.MouseEvent | React.TouchEvent) => { // Prevenir comportamientos por defecto del navegador if (e) { e.preventDefault() e.stopPropagation() } const activeTerminal = terminals.find((t) => t.id === activeTerminalId) if (!activeTerminal || !activeTerminal.ws || activeTerminal.ws.readyState !== WebSocket.OPEN) return let seq = "" switch (key) { case "UP": seq = "\x1bOA" break case "DOWN": seq = "\x1bOB" break case "RIGHT": seq = "\x1bOC" break case "LEFT": seq = "\x1bOD" break case "ESC": seq = "\x1b" break case "TAB": seq = "\t" break case "CTRL_C": seq = "\x03" break case "ENTER": seq = "\r" break default: break } activeTerminal.ws.send(seq) } const handleClear = () => { const activeTerminal = terminals.find((t) => t.id === activeTerminalId) if (activeTerminal?.term) { activeTerminal.term.clear() } } const handleClose = () => { terminals.forEach((terminal) => { // Clear heartbeat interval if (terminal.pingInterval) clearInterval(terminal.pingInterval) if (terminal.ws) terminal.ws.close() if (terminal.term) terminal.term.dispose() }) onClose?.() } const sendToActiveTerminal = (command: string) => { const activeTerminal = terminals.find((t) => t.id === activeTerminalId) if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) { activeTerminal.ws.send(command) setTimeout(() => { setSearchModalOpen(false) }, 100) } } const sendSequence = (seq: string, e?: React.MouseEvent | React.TouchEvent) => { if (e) { e.preventDefault() e.stopPropagation() } const activeTerminal = terminals.find((t) => t.id === activeTerminalId) if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) { activeTerminal.ws.send(seq) } } // Mobile clipboard helpers — desktop users have ctrl/cmd shortcuts via xterm, // but on touch devices xterm's selection / clipboard isn't reachable from the // OS clipboard manager so we expose explicit Copy / Paste buttons. const handleCopy = async (e?: React.MouseEvent | React.TouchEvent) => { if (e) { e.preventDefault() e.stopPropagation() } const activeTerminal = terminals.find((t) => t.id === activeTerminalId) await copyTerminalSelection(activeTerminal?.term) } const handlePaste = async (e?: React.MouseEvent | React.TouchEvent) => { if (e) { e.preventDefault() e.stopPropagation() } const activeTerminal = terminals.find((t) => t.id === activeTerminalId) if (!activeTerminal?.ws || activeTerminal.ws.readyState !== WebSocket.OPEN) return const ws = activeTerminal.ws await pasteFromClipboard((text) => ws.send(text)) } const getLayoutClass = () => { const count = terminals.length if (isMobile || count === 1) return "grid grid-cols-1" // Vista de cuadrícula 2x2 if (layout === "grid") { if (count === 2) return "grid grid-cols-2" if (count === 3) return "grid grid-cols-2 grid-rows-2" if (count === 4) return "grid grid-cols-2 grid-rows-2" } if (count === 2) return "grid grid-cols-1 grid-rows-2" if (count === 3) return "grid grid-cols-1 grid-rows-3" if (count === 4) return "grid grid-cols-1 grid-rows-4" // Vista de filas apiladas (single) - una terminal debajo de otra return "grid grid-cols-1" } const activeTerminal = terminals.find((t) => t.id === activeTerminalId) return (
{t("terminal.terminalCount", { count: terminals.length })}
{!isMobile && terminals.length > 1 && ( <> )}
{ containerRefs.current["main"] = el }} className={`overflow-hidden flex flex-col ${isMobile ? "flex-1 h-[60vh]" : "overflow-hidden"} w-full max-w-full`} style={!isMobile || isTablet ? { height: `${terminalHeight}px`, flexShrink: 0 } : undefined} > {isMobile ? ( {terminals.map((terminal) => ( {terminal.title} {terminals.length > 1 && ( )} ))} {terminals.map((terminal) => (
(containerRefs.current[terminal.id] = el)} className="w-full h-full flex-1 bg-black overflow-hidden" translate="no" /> ))} ) : (
{terminals.map((terminal) => (
1 && activeTerminalId === terminal.id ? "ring-2 ring-blue-500" : "" }`} >
{terminals.length > 1 && ( )}
(containerRefs.current[terminal.id] = el)} onClick={() => setActiveTerminalId(terminal.id)} className="flex-1 w-full max-w-full bg-black overflow-hidden cursor-pointer" translate="no" data-terminal-container />
))}
)}
{(isTablet || (!isMobile && !isTablet)) && terminals.length > 0 && (
)} {(isMobile || isTablet) && (
{t("scriptTerminal.controlSequences")} sendSequence("\x03")}> Ctrl+C {t("scriptTerminal.cancelInterrupt")} sendSequence("\x18")}> Ctrl+X {t("scriptTerminal.exitNano")} sendSequence("\x12")}> Ctrl+R {t("scriptTerminal.searchHistory")} {t("scriptTerminal.clipboard")} { void handleCopy() }}> {t("scriptTerminal.copySelection")} { void handlePaste() }}> {t("scriptTerminal.paste")}
)} {t("terminal.searchCommands")}
{t("terminal.searchDescription")}
setSearchQuery(e.target.value)} className="pl-10 bg-zinc-900 border-zinc-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 text-base" autoCapitalize="none" autoComplete="off" autoCorrect="off" spellCheck={false} />
{isSearching && (

{t("terminal.searchingCheatSh")}

)}
{searchResults.length > 0 ? ( <> {searchResults.map((result, index) => (
{result.description && (

# {result.description}

)}
sendToActiveTerminal(result.command)} className="flex items-start justify-between gap-2 cursor-pointer group hover:bg-zinc-800/50 rounded p-2 -m-2" > {result.command}
))}

{t("terminal.poweredByCheatSh")}

) : filteredCommands.length > 0 && !useOnline ? ( filteredCommands.map((item, index) => (
sendToActiveTerminal(item.cmd)} className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" >
{item.cmd}

{item.desc}

)) ) : !isSearching && !searchQuery && !useOnline ? ( localizedCommands.map((item, index) => (
sendToActiveTerminal(item.cmd)} className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" >
{item.cmd}

{item.desc}

)) ) : !isSearching ? (
{searchQuery ? ( <>

{t("terminal.noResults", { query: searchQuery })}

{t("terminal.tryDifferentSearch")}

) : ( <>

{t("terminal.searchAnyCommand")}

{t("terminal.trySearchingFor")}

{["tar", "grep", "docker", "qm", "systemctl"].map((cmd) => ( setSearchQuery(cmd)} className="px-2 py-1 bg-zinc-800 rounded text-blue-400 cursor-pointer hover:bg-zinc-700" > {cmd} ))}
{useOnline && (
{t("terminal.poweredByCheatSh")}
)} )}
) : null}
{t("terminal.searchTip")}
{useOnline && searchResults.length > 0 && {t("terminal.poweredByCheatSh")}}
) }