fix: unified fullscreen terminal header

- Add showControls prop to TerminalComponent to optionally hide internal header
- Add reset() method to TerminalRef for external reset control
- TerminalPage now renders a unified fullscreen header bar combining:
  - Session tabs (TerminalSessionTabs)
  - Terminal controls (status dot, A-, A+, Reset, Exit Fullscreen)
- Unified header is always visible in fullscreen (no hover-to-reveal)
- TerminalComponent internal header hidden when in fullscreen mode
- Remove old CSS that hid session tabs with opacity:0 until hover

Quality gates: pytest 188 passed, frontend typecheck clean

Fixes: terminal-fullscreen-unified-header
This commit is contained in:
Alex Blank
2026-05-29 10:34:31 +02:00
parent 0fdbef578f
commit 8926152fca
7 changed files with 297 additions and 95 deletions
+125 -9
View File
@@ -17,6 +17,8 @@ const SESSIONS_TO_INFO = (sessions: TerminalSession[]): TerminalSessionInfo[] =>
status: s.status as TerminalSessionInfo["status"],
}));
type TerminalStatus = "connecting" | "connected" | "disconnected" | "error" | "resetting";
export const TerminalPage: React.FC = () => {
const { instanceId } = useParams<{
instanceId: string;
@@ -27,6 +29,11 @@ export const TerminalPage: React.FC = () => {
const terminalRefs = useRef<Record<string, React.RefObject<TerminalRef>>>({});
const headerAutoHide = useAutoHide({ timeout: 3000, enabled: isMobile });
// Track terminal status and callbacks for unified fullscreen header
const [terminalStatuses, setTerminalStatuses] = useState<Record<string, TerminalStatus>>({});
const changeFontSizeRef = useRef<((delta: number) => void) | null>(null);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const {
sessions,
activeSessionId,
@@ -178,6 +185,29 @@ export const TerminalPage: React.FC = () => {
[renameSession],
);
const handleTerminalReady = useCallback(
(
_sendData: (data: string) => void,
status: TerminalStatus,
_focusInput: () => void,
changeFontSize: (delta: number) => void,
) => {
setTerminalStatuses((prev) => ({ ...prev, [activeSessionId ?? "default"]: status }));
changeFontSizeRef.current = changeFontSize;
},
[activeSessionId],
);
const handleFontSizeChange = useCallback((delta: number) => {
changeFontSizeRef.current?.(delta);
}, []);
const handleReset = useCallback(() => {
if (activeSessionId && terminalRefs.current[activeSessionId]) {
terminalRefs.current[activeSessionId].current?.reset();
}
}, [activeSessionId]);
if (!instanceId) {
return (
<section className="stack">
@@ -240,6 +270,7 @@ export const TerminalPage: React.FC = () => {
sessionId={session.id}
onClose={() => handleClose(session.id)}
isMobile={true}
onTerminalReady={handleTerminalReady}
/>
</div>
))}
@@ -275,15 +306,98 @@ export const TerminalPage: React.FC = () => {
</button>
</div>
)}
<TerminalSessionTabs
sessions={sessionInfos}
activeSessionId={activeSessionId ?? ""}
onSelect={handleSelect}
onClose={handleClose}
onCreate={handleCreate}
onRename={handleRename}
isMobile={false}
/>
{isFullscreen ? (
<div className="terminal-fullscreen-header">
<div className="terminal-fullscreen-header-tabs">
<TerminalSessionTabs
sessions={sessionInfos}
activeSessionId={activeSessionId ?? ""}
onSelect={handleSelect}
onClose={handleClose}
onCreate={handleCreate}
onRename={handleRename}
isMobile={false}
/>
</div>
<div className="terminal-fullscreen-header-controls">
<span
className={`terminal-fullscreen-status status-dot ${terminalStatuses[activeSessionId ?? "default"] ?? "connecting"}`}
aria-label={`Terminal status: ${terminalStatuses[activeSessionId ?? "default"] ?? "connecting"}`}
/>
<button
className="terminal-header-button"
onClick={() => handleFontSizeChange(-1)}
type="button"
aria-label="Decrease font size"
>
A-
</button>
<button
className="terminal-header-button"
onClick={() => handleFontSizeChange(1)}
type="button"
aria-label="Increase font size"
>
A+
</button>
<button
className="terminal-header-button"
onClick={() => setShowResetConfirm(true)}
type="button"
aria-label="Reset terminal"
>
Reset
</button>
<button
className="terminal-close"
onClick={() => setIsFullscreen(false)}
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={() => setShowResetConfirm(false)}
type="button"
>
Cancel
</button>
<button
className="terminal-reset-confirm-button confirm"
onClick={() => {
setShowResetConfirm(false);
handleReset();
}}
type="button"
>
Reset
</button>
</div>
</div>
</div>
)}
</div>
) : (
<TerminalSessionTabs
sessions={sessionInfos}
activeSessionId={activeSessionId ?? ""}
onSelect={handleSelect}
onClose={handleClose}
onCreate={handleCreate}
onRename={handleRename}
isMobile={false}
/>
)}
<div className="terminal-page-content">
{error && <div className="terminal-error-banner">{error}</div>}
{sessions
@@ -296,6 +410,8 @@ export const TerminalPage: React.FC = () => {
sessionId={session.id}
onClose={() => handleClose(session.id)}
isMobile={false}
showControls={!isFullscreen}
onTerminalReady={handleTerminalReady}
/>
</div>
))}