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 changeFontSizeRefs = useRef< Record void) | null> >({}); const sendDataRefs = useRef void) | null>>( {}, ); const focusInputRefs = useRef void) | null>>({}); const [showResetConfirm, setShowResetConfirm] = useState(false); const [showSpecialKeysPanel, setShowSpecialKeysPanel] = useState(false); const [activeModifier, setActiveModifier] = useState( null, ); const { isOpen: isKeyboardOpen, height: keyboardHeight } = useVirtualKeyboard(); const [instanceInfo, setInstanceInfo] = useState<{ display_name: string; workspace_name?: string | null; tool_type_name: string; } | null>(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 { const { getUserSessions } = await import("../api/sessions"); const allSessions = await getUserSessions(); const match = allSessions.find((s) => s.id === instanceId); if (match) { setInstanceInfo({ display_name: match.display_name, workspace_name: match.workspace_name, tool_type_name: match.tool_type_name, }); } } 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"; return; } const active = sessions.find((s) => s.id === activeSessionId); const baseName = instanceInfo ? `${instanceInfo.workspace_name ?? instanceInfo.display_name} ยท ${instanceInfo.tool_type_name}` : `Instance ${instanceId.slice(0, 8)}`; if (sessions.length <= 1) { document.title = baseName; } else { const sessionName = active?.name ?? "Session"; document.title = `${baseName} ${sessionName}`; } return () => { document.title = "Headquarter"; }; }, [instanceId, activeSessionId, sessions, instanceInfo]); // 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]; delete sendDataRefs.current[id]; delete focusInputRefs.current[id]; delete changeFontSizeRefs.current[id]; } } setTerminalStatuses((prev) => { const next: Record = {}; for (const id of currentIds) { if (prev[id]) { next[id] = prev[id]; } } return next; }); }, [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; const activeSessionIndex = activeSessionId ? sessions.findIndex((session) => session.id === activeSessionId) : -1; 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 (activeSessionIndex > 0) { setActiveSessionId(sessions[activeSessionIndex - 1].id); } break; case "arrowright": e.preventDefault(); if (activeSessionIndex >= 0 && activeSessionIndex < sessions.length - 1) { setActiveSessionId(sessions[activeSessionIndex + 1].id); } break; case "r": e.preventDefault(); if (activeSessionId) void resetSession(activeSessionId); break; case "f": e.preventDefault(); setIsFullscreen((prev) => !prev); break; default: 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( ( sessionId: string | undefined, sendData: (data: string) => void, status: TerminalStatus, focusInput: () => void, changeFontSize: (delta: number) => void, ) => { const key = sessionId ?? "default"; setTerminalStatuses((prev) => ({ ...prev, [key]: status, })); sendDataRefs.current[key] = sendData; focusInputRefs.current[key] = focusInput; changeFontSizeRefs.current[key] = changeFontSize; }, [], ); const handleFontSizeChange = useCallback( (delta: number) => { const key = activeSessionId ?? "default"; changeFontSizeRefs.current[key]?.(delta); }, [activeSessionId], ); const handleSendKey = useCallback( (data: string) => { const key = activeSessionId ?? "default"; sendDataRefs.current[key]?.(data); }, [activeSessionId], ); const handleReset = useCallback(() => { if (activeSessionId && terminalRefs.current[activeSessionId]) { terminalRefs.current[activeSessionId].current?.reset(); } }, [activeSessionId]); return { instanceId, navigate, isMobile, isFullscreen, setIsFullscreen, terminalRefs, headerAutoHide, terminalStatuses, 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), }; };