import React, { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import type { TerminalRef } from "../components/features/terminal/terminal"; import type { TerminalSessionInfo } from "../components/features/terminal/terminal-session-tabs"; import { useMobileViewport } from "./use-mobile-viewport"; import { useAutoHide } from "./use-auto-hide"; import { useVirtualKeyboard } from "./use-virtual-keyboard"; import { useTerminalSessions } from "./use-terminal-sessions"; import type { TerminalSession } from "../api/terminal"; import type { ModifierKey } from "./use-special-keys"; const SESSIONS_TO_INFO = (sessions: TerminalSession[]): TerminalSessionInfo[] => sessions.map((s) => ({ id: s.id, name: s.name, status: s.status as TerminalSessionInfo["status"], })); type TerminalStatus = | "connecting" | "connected" | "disconnected" | "error" | "resetting"; export const useTerminalPage = () => { const { instanceId } = useParams<{ instanceId: string }>(); const navigate = useNavigate(); const isMobile = useMobileViewport(); const [isFullscreen, setIsFullscreen] = useState(false); const terminalRefs = useRef>>({}); const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile }); const [terminalStatuses, setTerminalStatuses] = useState< Record >({}); const changeFontSizeRef = useRef<((delta: number) => void) | null>(null); const sendDataRef = useRef<((data: string) => void) | null>(null); const focusInputRef = useRef<(() => void) | null>(null); const [showResetConfirm, setShowResetConfirm] = useState(false); const [showSpecialKeysPanel, setShowSpecialKeysPanel] = useState(false); const [activeModifier, setActiveModifier] = useState( null, ); const { isOpen: isKeyboardOpen, height: keyboardHeight } = useVirtualKeyboard(); const [instanceDisplayName, setInstanceDisplayName] = useState( null, ); const { sessions, activeSessionId, setActiveSessionId, createSession, closeSession, renameSession, resetSession, loading, error, } = useTerminalSessions(instanceId ?? ""); // Fetch instance details for tab title useEffect(() => { if (!instanceId) return; const load = async () => { try { // Try to find instance in user sessions first const { getUserSessions } = await import("../api/sessions"); const allSessions = await getUserSessions(); const match = allSessions.find((s) => s.id === instanceId); if (match) { setInstanceDisplayName(match.display_name); return; } } catch { // ignore } }; void load(); }, [instanceId]); // Auto-create default session useEffect(() => { if (!loading && sessions.length === 0 && !error && instanceId) { void createSession("Session 1"); } }, [loading, sessions.length, error, instanceId, createSession]); // Update document title based on active terminal session useEffect(() => { if (!instanceId) { document.title = "Terminal — Headquarter"; return; } const active = sessions.find((s) => s.id === activeSessionId); const sessionName = active?.name ?? "Session"; const baseName = instanceDisplayName ?? `Instance ${instanceId.slice(0, 8)}`; document.title = `${baseName} ${sessionName} — Terminal — Headquarter`; return () => { document.title = "Headquarter"; }; }, [instanceId, activeSessionId, sessions, instanceDisplayName]); // Sync refs with sessions useEffect(() => { for (const session of sessions) { if (!terminalRefs.current[session.id]) { terminalRefs.current[session.id] = React.createRef(); } } const currentIds = new Set(sessions.map((s) => s.id)); for (const id of Object.keys(terminalRefs.current)) { if (!currentIds.has(id)) { delete terminalRefs.current[id]; } } }, [sessions]); // Fit and focus active terminal useEffect(() => { if (activeSessionId && terminalRefs.current[activeSessionId]) { const ref = terminalRefs.current[activeSessionId]; let raf1 = 0; let raf2 = 0; raf1 = requestAnimationFrame(() => { raf2 = requestAnimationFrame(() => { ref.current?.fit(); ref.current?.focus(); }); }); return () => { cancelAnimationFrame(raf1); cancelAnimationFrame(raf2); }; } }, [activeSessionId]); // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey; if (!isAltShift) return; switch (e.key.toLowerCase()) { case "n": e.preventDefault(); if (sessions.length < 5) { void createSession(`Session ${sessions.length + 1}`); } break; case "w": e.preventDefault(); if ( activeSessionId && window.confirm("Close this terminal session?") ) { void closeSession(activeSessionId); } break; case "arrowleft": e.preventDefault(); if (activeSessionId) { const idx = sessions.findIndex((s) => s.id === activeSessionId); if (idx > 0) setActiveSessionId(sessions[idx - 1].id); } break; case "arrowright": e.preventDefault(); if (activeSessionId) { const idx = sessions.findIndex((s) => s.id === activeSessionId); if (idx < sessions.length - 1) setActiveSessionId(sessions[idx + 1].id); } break; case "r": e.preventDefault(); if (activeSessionId) void resetSession(activeSessionId); break; case "f": e.preventDefault(); setIsFullscreen((prev) => !prev); break; } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [ sessions, activeSessionId, createSession, closeSession, resetSession, setActiveSessionId, ]); // Keep screen awake useEffect(() => { let wakeLock: WakeLockSentinel | null = null; const requestWakeLock = async () => { try { if ("wakeLock" in navigator) { wakeLock = await navigator.wakeLock.request("screen"); } } catch { // ignore } }; void requestWakeLock(); const handleVisibilityChange = () => { if (document.visibilityState === "visible") void requestWakeLock(); }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); wakeLock?.release().catch(() => {}); }; }, []); // Lock page scroll on mobile useEffect(() => { if (!isMobile) return; document.documentElement.classList.add("terminal-page-open"); document.body.classList.add("terminal-page-open"); return () => { document.documentElement.classList.remove("terminal-page-open"); document.body.classList.remove("terminal-page-open"); }; }, [isMobile]); const handleFullscreenClick = useCallback( (e: React.MouseEvent) => { if (!isFullscreen) return; const target = e.target as Node; const current = e.currentTarget as HTMLElement; const content = current.querySelector(".terminal-page-content"); const header = current.querySelector(".terminal-fullscreen-header"); if (content?.contains(target) || header?.contains(target)) return; setIsFullscreen(false); }, [isFullscreen], ); const handleSelect = useCallback( (sessionId: string) => setActiveSessionId(sessionId), [setActiveSessionId], ); const handleClose = useCallback( async (sessionId: string) => closeSession(sessionId), [closeSession], ); const handleCreate = useCallback(() => { void createSession(`Session ${sessions.length + 1}`); }, [createSession, sessions.length]); const handleRename = useCallback( (sessionId: string, newName: string) => { void renameSession(sessionId, newName); }, [renameSession], ); const handleTerminalReady = useCallback( ( sendData: (data: string) => void, status: TerminalStatus, focusInput: () => void, changeFontSize: (delta: number) => void, ) => { setTerminalStatuses((prev) => ({ ...prev, [activeSessionId ?? "default"]: status, })); sendDataRef.current = sendData; focusInputRef.current = focusInput; changeFontSizeRef.current = changeFontSize; }, [activeSessionId], ); const handleFontSizeChange = useCallback((delta: number) => { changeFontSizeRef.current?.(delta); }, []); const handleSendKey = useCallback((data: string) => { sendDataRef.current?.(data); }, []); const handleReset = useCallback(() => { if (activeSessionId && terminalRefs.current[activeSessionId]) { terminalRefs.current[activeSessionId].current?.reset(); } }, [activeSessionId]); return { instanceId, navigate, isMobile, isFullscreen, setIsFullscreen, terminalRefs, headerAutoHide, terminalStatuses, sendDataRef, focusInputRef, changeFontSizeRef, showResetConfirm, setShowResetConfirm, showSpecialKeysPanel, setShowSpecialKeysPanel, activeModifier, setActiveModifier, isKeyboardOpen, keyboardHeight, sessions, activeSessionId, setActiveSessionId, loading, error, handleFullscreenClick, handleSelect, handleClose, handleCreate, handleRename, handleTerminalReady, handleFontSizeChange, handleSendKey, handleReset, sessionInfos: SESSIONS_TO_INFO(sessions), }; };