import React, { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { TerminalComponent, type TerminalRef } from "../components/terminal"; import { TerminalSessionTabs, type TerminalSessionInfo, } from "../components/terminal-session-tabs"; import { useMobileViewport } from "../hooks/use-mobile-viewport"; import { useAutoHide } from "../hooks/use-auto-hide"; import { useTerminalSessions } from "../hooks/use-terminal-sessions"; import type { TerminalSession } from "../api/terminal"; const SESSIONS_TO_INFO = (sessions: TerminalSession[]): TerminalSessionInfo[] => sessions.map((s) => ({ id: s.id, name: s.name, status: s.status as TerminalSessionInfo["status"], })); export const TerminalPage: React.FC = () => { 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 { sessions, activeSessionId, setActiveSessionId, createSession, closeSession, renameSession, resetSession, loading, error, } = useTerminalSessions(instanceId ?? ""); // Auto-create default session if none exist after loading completes useEffect(() => { if (!loading && sessions.length === 0 && !error && instanceId) { void createSession("Session 1"); } }, [loading, sessions.length, error, instanceId, createSession]); // Ensure refs map is kept in sync with sessions useEffect(() => { for (const session of sessions) { if (!terminalRefs.current[session.id]) { terminalRefs.current[session.id] = React.createRef(); } } // Clean up refs for closed sessions 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 when switching tabs useEffect(() => { if (activeSessionId && terminalRefs.current[activeSessionId]) { const ref = terminalRefs.current[activeSessionId]; // Small delay to allow display:block to apply const timer = setTimeout(() => { ref.current?.fit(); ref.current?.focus(); }, 50); return () => clearTimeout(timer); } }, [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; default: break; } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [ sessions, activeSessionId, createSession, closeSession, resetSession, setActiveSessionId, ]); // Exit fullscreen on Escape useEffect(() => { if (!isFullscreen) return; const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") { setIsFullscreen(false); } }; window.addEventListener("keydown", handleEscape); return () => window.removeEventListener("keydown", handleEscape); }, [isFullscreen]); const handleSelect = useCallback( (sessionId: string) => { setActiveSessionId(sessionId); }, [setActiveSessionId], ); const handleClose = useCallback( async (sessionId: string) => { await 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], ); if (!instanceId) { return (

Terminal

No instance ID provided.

); } const sessionInfos = SESSIONS_TO_INFO(sessions); if (isMobile) { return (
headerAutoHide.show()} >

Terminal

headerAutoHide.show()} >
{error &&
{error}
} {sessions .filter((session) => session.id === activeSessionId) .map((session) => (
handleClose(session.id)} isMobile={true} />
))} {sessions.length === 0 && !loading && (

No terminal sessions. Press Alt+Shift+N to create one.

)}
); } return (
{!isFullscreen && (

Terminal

)}
{error &&
{error}
} {sessions .filter((session) => session.id === activeSessionId) .map((session) => (
handleClose(session.id)} isMobile={false} />
))} {sessions.length === 0 && !loading && (

No terminal sessions. Press Alt+Shift+N to create one.

)}
); };