diff --git a/apps/web/src/components/features/terminal/DesktopTerminalView.tsx b/apps/web/src/components/features/terminal/DesktopTerminalView.tsx new file mode 100644 index 0000000..829d345 --- /dev/null +++ b/apps/web/src/components/features/terminal/DesktopTerminalView.tsx @@ -0,0 +1,198 @@ +import React from "react"; +import { TerminalComponent, type TerminalRef } from "./terminal"; +import { TerminalSessionTabs, type TerminalSessionInfo } from "./terminal-session-tabs"; +import type { TerminalSession } from "../../../api/terminal"; + +interface Props { + instanceId: string; + sessions: TerminalSession[]; + sessionInfos: TerminalSessionInfo[]; + activeSessionId: string; + terminalRefs: React.MutableRefObject>>; + isFullscreen: boolean; + status: string; + error: string | null; + loading: boolean; + showResetConfirm: boolean; + onFullscreenClick: (e: React.MouseEvent) => void; + onSelect: (id: string) => void; + onClose: (id: string) => void; + onCreate: () => void; + onRename: (id: string, name: string) => void; + onNavigateBack: () => void; + onToggleFullscreen: () => void; + onFontSizeChange: (delta: number) => void; + onShowResetConfirm: () => void; + onHideResetConfirm: () => void; + onReset: () => void; + onTerminalReady: ( + sendData: (data: string) => void, + status: "connecting" | "connected" | "disconnected" | "error" | "resetting", + focusInput: () => void, + changeFontSize: (delta: number) => void, + ) => void; +} + +export const DesktopTerminalView: React.FC = ({ + instanceId, + sessions, + sessionInfos, + activeSessionId, + terminalRefs, + isFullscreen, + status, + error, + loading, + showResetConfirm, + onFullscreenClick, + onSelect, + onClose, + onCreate, + onRename, + onNavigateBack, + onToggleFullscreen, + onFontSizeChange, + onShowResetConfirm, + onHideResetConfirm, + onReset, + onTerminalReady, +}) => { + return ( +
+ {!isFullscreen && ( +
+ +

Terminal

+ +
+ )} + {isFullscreen ? ( +
+
+ +
+
+ + + + + +
+ {showResetConfirm && ( +
+
+

+ Reset terminal? This will kill the current shell session and + start fresh. +

+
+ + +
+
+
+ )} +
+ ) : ( + + )} +
+ {error &&
{error}
} + {sessions + .filter((session) => session.id === activeSessionId) + .map((session) => ( +
+ onClose(session.id)} + isMobile={false} + showControls={!isFullscreen} + onTerminalReady={onTerminalReady} + /> +
+ ))} + {sessions.length === 0 && !loading && ( +
+

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

+
+ )} +
+
+ ); +}; diff --git a/apps/web/src/components/features/terminal/MobileTerminalView.tsx b/apps/web/src/components/features/terminal/MobileTerminalView.tsx new file mode 100644 index 0000000..b04586e --- /dev/null +++ b/apps/web/src/components/features/terminal/MobileTerminalView.tsx @@ -0,0 +1,161 @@ +import React from "react"; +import { TerminalComponent, type TerminalRef } from "./terminal"; +import { TerminalSessionTabs, type TerminalSessionInfo } from "./terminal-session-tabs"; +import { Icon } from "../../icon"; +import { SpecialKeysStrip } from "./special-keys-strip"; +import { SpecialKeysPanel } from "./special-keys-panel"; +import type { ModifierKey } from "../../../hooks/use-special-keys"; +import type { TerminalSession } from "../../../api/terminal"; + +interface Props { + instanceId: string; + sessions: TerminalSession[]; + sessionInfos: TerminalSessionInfo[]; + activeSessionId: string; + terminalRefs: React.MutableRefObject>>; + status: string; + error: string | null; + loading: boolean; + isKeyboardOpen: boolean; + keyboardHeight: number; + isVisible: boolean; + activeModifier: ModifierKey | null; + showSpecialKeysPanel: boolean; + onToggleHeader: () => void; + onNavigateBack: () => void; + onFontSizeChange: (delta: number) => void; + onSelect: (id: string) => void; + onClose: (id: string) => void; + onCreate: () => void; + onRename: (id: string, name: string) => void; + onTerminalReady: ( + sendData: (data: string) => void, + status: "connecting" | "connected" | "disconnected" | "error" | "resetting", + focusInput: () => void, + changeFontSize: (delta: number) => void, + ) => void; + onSendKey: (data: string) => void; + onModifierChange: (mod: ModifierKey | null) => void; + onShowSpecialKeys: () => void; + onHideSpecialKeys: () => void; + onKeepFocus: () => void; +} + +export const MobileTerminalView: React.FC = ({ + instanceId, + sessions, + sessionInfos, + activeSessionId, + terminalRefs, + status, + error, + loading, + isKeyboardOpen, + keyboardHeight, + isVisible, + activeModifier, + showSpecialKeysPanel, + onToggleHeader, + onNavigateBack, + onFontSizeChange, + onSelect, + onClose, + onCreate, + onRename, + onTerminalReady, + onSendKey, + onModifierChange, + onShowSpecialKeys, + onHideSpecialKeys, + onKeepFocus, +}) => { + const activeSession = sessions.find((s) => s.id === activeSessionId); + + return ( +
+
e.stopPropagation()}> +
+
+ +
+
+ {activeSession?.name || "Terminal"} + +
+
+ + + +
+
+
+ +
+
+ +
+ {error &&
{error}
} + {sessions + .filter((session) => session.id === activeSessionId) + .map((session) => ( +
+ onClose(session.id)} + isMobile={true} + showControls={false} + activeModifier={activeModifier} + onModifierChange={onModifierChange} + onTerminalReady={onTerminalReady} + /> +
+ ))} + {sessions.length === 0 && !loading && ( +
+

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

+
+ )} +
+ + + + +
+ ); +}; diff --git a/apps/web/src/hooks/use-terminal-page.ts b/apps/web/src/hooks/use-terminal-page.ts new file mode 100644 index 0000000..b6a8414 --- /dev/null +++ b/apps/web/src/hooks/use-terminal-page.ts @@ -0,0 +1,289 @@ +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 { + sessions, + activeSessionId, + setActiveSessionId, + createSession, + closeSession, + renameSession, + resetSession, + loading, + error, + } = useTerminalSessions(instanceId ?? ""); + + // Auto-create default session + useEffect(() => { + if (!loading && sessions.length === 0 && !error && instanceId) { + void createSession("Session 1"); + } + }, [loading, sessions.length, error, instanceId, createSession]); + + // 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), + }; +}; diff --git a/apps/web/src/pages/TerminalPage.tsx b/apps/web/src/pages/TerminalPage.tsx index 0137ac6..d1a79e7 100644 --- a/apps/web/src/pages/TerminalPage.tsx +++ b/apps/web/src/pages/TerminalPage.tsx @@ -1,571 +1,112 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { TerminalComponent, type TerminalRef } from "../components/features/terminal/terminal"; -import { - TerminalSessionTabs, - type TerminalSessionInfo, -} from "../components/features/terminal/terminal-session-tabs"; -import { Icon } from "../components/icon"; -import { SpecialKeysStrip } from "../components/features/terminal/special-keys-strip"; -import { SpecialKeysPanel } from "../components/features/terminal/special-keys-panel"; -import { useMobileViewport } from "../hooks/use-mobile-viewport"; -import { useAutoHide } from "../hooks/use-auto-hide"; -import { useVirtualKeyboard } from "../hooks/use-virtual-keyboard"; -import { useTerminalSessions } from "../hooks/use-terminal-sessions"; -import type { TerminalSession } from "../api/terminal"; -import type { ModifierKey } from "../hooks/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"; +import React from "react"; +import { useTerminalPage } from "../hooks/use-terminal-page"; +import { MobileTerminalView } from "../components/features/terminal/MobileTerminalView"; +import { DesktopTerminalView } from "../components/features/terminal/DesktopTerminalView"; 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 { + instanceId, + navigate, + isMobile, + isFullscreen, + setIsFullscreen, + terminalRefs, + headerAutoHide, + terminalStatuses, + showResetConfirm, + setShowResetConfirm, + showSpecialKeysPanel, + setShowSpecialKeysPanel, + activeModifier, + setActiveModifier, + isKeyboardOpen, + keyboardHeight, + sessions, + activeSessionId, + loading, + error, + handleFullscreenClick, + handleSelect, + handleClose, + handleCreate, + handleRename, + handleTerminalReady, + handleFontSizeChange, + handleSendKey, + handleReset, + sessionInfos, + } = useTerminalPage(); - // Track terminal status and callbacks for unified fullscreen header - 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(); + if (!instanceId) { + return ( +
+

Terminal

+

No instance ID provided.

+
+ ); + } - const { - sessions, - activeSessionId, - setActiveSessionId, - createSession, - closeSession, - renameSession, - resetSession, - loading, - error, - } = useTerminalSessions(instanceId ?? ""); + const status = terminalStatuses[activeSessionId ?? "default"] ?? "connecting"; - // 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]); + if (isMobile) { + return ( + navigate("/sessions")} + onFontSizeChange={handleFontSizeChange} + onSelect={handleSelect} + onClose={handleClose} + onCreate={handleCreate} + onRename={handleRename} + onTerminalReady={handleTerminalReady} + onSendKey={handleSendKey} + onModifierChange={setActiveModifier} + onShowSpecialKeys={() => setShowSpecialKeysPanel(true)} + onHideSpecialKeys={() => setShowSpecialKeysPanel(false)} + onKeepFocus={() => { + /* focus handled by ref */ + }} + /> + ); + } - // 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]; - // Double rAF ensures layout has settled after the display:block switch - 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; - default: - break; - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [ - sessions, - activeSessionId, - createSession, - closeSession, - resetSession, - setActiveSessionId, - ]); - - // Keep screen awake while terminal is open - useEffect(() => { - let wakeLock: WakeLockSentinel | null = null; - - const requestWakeLock = async () => { - try { - if ("wakeLock" in navigator) { - wakeLock = await navigator.wakeLock.request("screen"); - } - } catch { - // Wake lock may be denied; silently 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 terminal so swipes scroll the terminal buffer, - // not the page. - 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]); - - // Click outside terminal content/header to exit fullscreen - 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) => { - 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], - ); - - 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]); - - if (!instanceId) { - return ( -
-

Terminal

-

No instance ID provided.

-
- ); - } - - const sessionInfos = SESSIONS_TO_INFO(sessions); - - if (isMobile) { - const activeSession = sessions.find((s) => s.id === activeSessionId); - const status = - terminalStatuses[activeSessionId ?? "default"] ?? "connecting"; - - return ( -
- {/* Overlay status bar — floats over terminal, never resizes it */} -
e.stopPropagation()} - > -
-
- -
-
- - {activeSession?.name || "Terminal"} - - -
-
- - - -
-
-
- -
-
- - {/* Terminal content — always fills full viewport */} -
headerAutoHide.toggle()} - > - {error &&
{error}
} - {sessions - .filter((session) => session.id === activeSessionId) - .map((session) => ( -
- handleClose(session.id)} - isMobile={true} - showControls={false} - activeModifier={activeModifier} - onModifierChange={setActiveModifier} - onTerminalReady={handleTerminalReady} - /> -
- ))} - {sessions.length === 0 && !loading && ( -
-

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

-
- )} -
- - setShowSpecialKeysPanel(true)} - onKeepFocus={() => focusInputRef.current?.()} - activeModifier={activeModifier} - onModifierChange={setActiveModifier} - /> - - setShowSpecialKeysPanel(false)} - onKeepFocus={() => focusInputRef.current?.()} - activeModifier={activeModifier} - onModifierChange={setActiveModifier} - /> -
- ); - } - - return ( -
- {!isFullscreen && ( -
- -

Terminal

- -
- )} - {isFullscreen ? ( -
-
- -
-
- - - - - -
- {showResetConfirm && ( -
-
-

- Reset terminal? This will kill the current shell session and - start fresh. -

-
- - -
-
-
- )} -
- ) : ( - - )} -
- {error &&
{error}
} - {sessions - .filter((session) => session.id === activeSessionId) - .map((session) => ( -
- handleClose(session.id)} - isMobile={false} - showControls={!isFullscreen} - onTerminalReady={handleTerminalReady} - /> -
- ))} - {sessions.length === 0 && !loading && ( -
-

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

-
- )} -
-
- ); + return ( + navigate("/sessions")} + onToggleFullscreen={() => setIsFullscreen((p) => !p)} + onFontSizeChange={handleFontSizeChange} + onShowResetConfirm={() => setShowResetConfirm(true)} + onHideResetConfirm={() => setShowResetConfirm(false)} + onReset={handleReset} + onTerminalReady={handleTerminalReady} + /> + ); };