refactor: extract TerminalPage components

- Extract use-terminal-page hook for terminal state and effects
- Extract MobileTerminalView and DesktopTerminalView components
- Slim TerminalPage from 571 to 112 lines

Quality gates: tsc --noEmit passes, npm run build passes
This commit is contained in:
Developer
2026-06-05 19:17:04 +00:00
parent d7d5baa41a
commit e7f219f7c3
4 changed files with 753 additions and 564 deletions
@@ -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<Record<string, React.RefObject<TerminalRef>>>;
isFullscreen: boolean;
status: string;
error: string | null;
loading: boolean;
showResetConfirm: boolean;
onFullscreenClick: (e: React.MouseEvent<HTMLElement>) => 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<Props> = ({
instanceId,
sessions,
sessionInfos,
activeSessionId,
terminalRefs,
isFullscreen,
status,
error,
loading,
showResetConfirm,
onFullscreenClick,
onSelect,
onClose,
onCreate,
onRename,
onNavigateBack,
onToggleFullscreen,
onFontSizeChange,
onShowResetConfirm,
onHideResetConfirm,
onReset,
onTerminalReady,
}) => {
return (
<section
className={`terminal-page ${isFullscreen ? "fullscreen" : ""}`}
onClick={onFullscreenClick}
>
{!isFullscreen && (
<div className="terminal-page-header">
<button className="secondary-button" onClick={onNavigateBack} type="button">
Back
</button>
<h1>Terminal</h1>
<button
className="secondary-button"
onClick={onToggleFullscreen}
type="button"
title="Toggle fullscreen (Alt+Shift+F)"
>
{isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
</button>
</div>
)}
{isFullscreen ? (
<div className="terminal-fullscreen-header">
<div className="terminal-fullscreen-header-tabs">
<TerminalSessionTabs
sessions={sessionInfos}
activeSessionId={activeSessionId}
onSelect={onSelect}
onClose={onClose}
onCreate={onCreate}
onRename={onRename}
isMobile={false}
/>
</div>
<div className="terminal-fullscreen-header-controls">
<span
className={`terminal-fullscreen-status status-dot ${status}`}
aria-label={`Terminal status: ${status}`}
/>
<button
className="terminal-header-button"
onClick={() => onFontSizeChange(-1)}
type="button"
aria-label="Decrease font size"
>
A-
</button>
<button
className="terminal-header-button"
onClick={() => onFontSizeChange(1)}
type="button"
aria-label="Increase font size"
>
A+
</button>
<button
className="terminal-header-button"
onClick={onShowResetConfirm}
type="button"
aria-label="Reset terminal"
>
Reset
</button>
<button
className="terminal-close"
onClick={onToggleFullscreen}
type="button"
title="Exit fullscreen (Esc)"
>
Exit
</button>
</div>
{showResetConfirm && (
<div className="terminal-reset-confirm">
<div className="terminal-reset-confirm-content">
<p>
Reset terminal? This will kill the current shell session and
start fresh.
</p>
<div className="terminal-reset-confirm-buttons">
<button
className="terminal-reset-confirm-button cancel"
onClick={onHideResetConfirm}
type="button"
>
Cancel
</button>
<button
className="terminal-reset-confirm-button confirm"
onClick={() => {
onHideResetConfirm();
onReset();
}}
type="button"
>
Reset
</button>
</div>
</div>
</div>
)}
</div>
) : (
<TerminalSessionTabs
sessions={sessionInfos}
activeSessionId={activeSessionId}
onSelect={onSelect}
onClose={onClose}
onCreate={onCreate}
onRename={onRename}
isMobile={false}
/>
)}
<div className="terminal-page-content">
{error && <div className="terminal-error-banner">{error}</div>}
{sessions
.filter((session) => session.id === activeSessionId)
.map((session) => (
<div key={session.id} className="terminal-instance active">
<TerminalComponent
ref={terminalRefs.current[session.id]}
instanceId={instanceId}
sessionId={session.id}
onClose={() => onClose(session.id)}
isMobile={false}
showControls={!isFullscreen}
onTerminalReady={onTerminalReady}
/>
</div>
))}
{sessions.length === 0 && !loading && (
<div className="terminal-empty-state">
<p>No terminal sessions. Press Alt+Shift+N to create one.</p>
</div>
)}
</div>
</section>
);
};
@@ -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<Record<string, React.RefObject<TerminalRef>>>;
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<Props> = ({
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 (
<section className="terminal-page mobile">
<div className={`mobile-terminal-overlay ${isVisible ? "visible" : "hidden"}`} onClick={(e) => e.stopPropagation()}>
<div className="mobile-terminal-toolbar">
<div className="mobile-terminal-toolbar-left">
<button className="mobile-terminal-toolbtn" onClick={onNavigateBack} type="button" aria-label="Back">
<Icon name="arrow-left" size="sm" />
</button>
</div>
<div className="mobile-terminal-toolbar-center">
<span className="mobile-terminal-title">{activeSession?.name || "Terminal"}</span>
<span className={`mobile-terminal-status status-dot ${status}`} aria-label={`Connection status: ${status}`} />
</div>
<div className="mobile-terminal-toolbar-right">
<button className="mobile-terminal-toolbtn" onClick={() => onFontSizeChange(-1)} type="button" aria-label="Decrease font size">
<span style={{ fontSize: "0.75rem" }}>A-</span>
</button>
<button className="mobile-terminal-toolbtn" onClick={() => onFontSizeChange(1)} type="button" aria-label="Increase font size">
<span style={{ fontSize: "1rem" }}>A+</span>
</button>
<button className="mobile-terminal-toolbtn" onClick={onNavigateBack} type="button" aria-label="Exit terminal">
<Icon name="close" size="sm" />
</button>
</div>
</div>
<div className="mobile-terminal-overlay-tabs">
<TerminalSessionTabs
sessions={sessionInfos}
activeSessionId={activeSessionId}
onSelect={onSelect}
onClose={onClose}
onCreate={onCreate}
onRename={onRename}
isMobile={true}
/>
</div>
</div>
<div
className="terminal-page-content mobile-full"
style={{ paddingBottom: isKeyboardOpen ? keyboardHeight : 0 }}
onClick={onToggleHeader}
>
{error && <div className="terminal-error-banner">{error}</div>}
{sessions
.filter((session) => session.id === activeSessionId)
.map((session) => (
<div key={session.id} className="terminal-instance active">
<TerminalComponent
ref={terminalRefs.current[session.id]}
instanceId={instanceId}
sessionId={session.id}
onClose={() => onClose(session.id)}
isMobile={true}
showControls={false}
activeModifier={activeModifier}
onModifierChange={onModifierChange}
onTerminalReady={onTerminalReady}
/>
</div>
))}
{sessions.length === 0 && !loading && (
<div className="terminal-empty-state">
<p>No terminal sessions. Press Alt+Shift+N to create one.</p>
</div>
)}
</div>
<SpecialKeysStrip
onSend={onSendKey}
isVisible={!showSpecialKeysPanel}
onMoreClick={onShowSpecialKeys}
onKeepFocus={onKeepFocus}
activeModifier={activeModifier}
onModifierChange={onModifierChange}
/>
<SpecialKeysPanel
onSend={onSendKey}
isOpen={showSpecialKeysPanel}
onClose={onHideSpecialKeys}
onKeepFocus={onKeepFocus}
activeModifier={activeModifier}
onModifierChange={onModifierChange}
/>
</section>
);
};