import { useEffect, useRef } from "react"; import { canHibernateTerminalRuntimeStatus, type TerminalHibernateWakePayload, } from "../../domain/terminalHibernate"; import { logger } from "../../lib/logger"; import { resolvePaneVisible, subscribePaneVisible, } from "./paneVisibilityStore"; import type { TerminalSession } from "../../types"; type UseTerminalHibernateEffectOptions = { sessionId: string; isVisible: boolean; isVisibleRef: React.MutableRefObject; getSessionConnectedRef: React.MutableRefObject<() => boolean>; status: TerminalSession["status"]; isSearchOpen: boolean; hibernateEnabled: boolean; hibernateDelayMs: number; fileTransferActive: boolean; hibernatedRef: React.MutableRefObject; softHiddenRef: React.MutableRefObject; hibernatePendingBufferRef: React.MutableRefObject; hibernateSnapshotRef: React.MutableRefObject; hibernateViewportSnapshotRef: React.MutableRefObject; hibernateScrollbackSnapshotRef: React.MutableRefObject; hibernateContextSnapshotRef: React.MutableRefObject; hibernateContextViewportSnapshotRef: React.MutableRefObject; hibernateContextScrollbackSnapshotRef: React.MutableRefObject; hibernateAlternateScreenRef: React.MutableRefObject; hasRuntimeRef: React.MutableRefObject; onHibernate: () => void; onSoftHideWake: () => void; onWake: ( getPayload: () => TerminalHibernateWakePayload, options: { sessionConnected: boolean }, ) => boolean | Promise; }; export function useTerminalHibernateEffect({ sessionId, isVisible, isVisibleRef, getSessionConnectedRef, status, isSearchOpen, hibernateEnabled, hibernateDelayMs, fileTransferActive, hibernatedRef, softHiddenRef, hibernatePendingBufferRef, hibernateSnapshotRef, hibernateViewportSnapshotRef, hibernateScrollbackSnapshotRef, hibernateContextSnapshotRef, hibernateContextViewportSnapshotRef, hibernateContextScrollbackSnapshotRef, hibernateAlternateScreenRef, hasRuntimeRef, onHibernate, onSoftHideWake, onWake, }: UseTerminalHibernateEffectOptions): void { const hiddenSinceRef = useRef(null); const hibernateTimerRef = useRef(null); const paneVisibleRef = useRef(resolvePaneVisible(sessionId, isVisible)); const onHibernateRef = useRef(onHibernate); const onSoftHideWakeRef = useRef(onSoftHideWake); const onWakeRef = useRef(onWake); onHibernateRef.current = onHibernate; onSoftHideWakeRef.current = onSoftHideWake; onWakeRef.current = onWake; useEffect(() => { const resolveVisible = () => resolvePaneVisible(sessionId, isVisibleRef.current); const clearHibernateTimer = () => { if (hibernateTimerRef.current !== null) { window.clearTimeout(hibernateTimerRef.current); hibernateTimerRef.current = null; } }; const clearHibernateState = () => { hibernateSnapshotRef.current = ""; hibernateViewportSnapshotRef.current = ""; hibernateScrollbackSnapshotRef.current = ""; hibernateContextSnapshotRef.current = ""; hibernateContextViewportSnapshotRef.current = ""; hibernateContextScrollbackSnapshotRef.current = ""; hibernatePendingBufferRef.current = ""; hibernateAlternateScreenRef.current = false; hibernatedRef.current = false; }; const scheduleHibernate = () => { if (!hibernateEnabled) return; clearHibernateTimer(); if (hibernatedRef.current || !hasRuntimeRef.current) return; if (!canHibernateTerminalRuntimeStatus(status)) return; if (isSearchOpen) return; if (fileTransferActive) return; hiddenSinceRef.current = Date.now(); const hiddenAt = hiddenSinceRef.current; hibernateTimerRef.current = window.setTimeout(() => { hibernateTimerRef.current = null; if (hiddenSinceRef.current !== hiddenAt) return; if (resolveVisible()) return; onHibernateRef.current(); }, hibernateDelayMs); }; const tryWake = () => { if (softHiddenRef.current) { onSoftHideWakeRef.current(); return; } if (!hibernatedRef.current) return; const sessionConnected = getSessionConnectedRef.current(); const getPayload = (): TerminalHibernateWakePayload => ({ snapshot: hibernateSnapshotRef.current, viewportSnapshot: hibernateViewportSnapshotRef.current || hibernateSnapshotRef.current, scrollbackSnapshot: hibernateScrollbackSnapshotRef.current, pendingBuffer: hibernatePendingBufferRef.current, alternateScreen: hibernateAlternateScreenRef.current, }); logger.info("[Terminal] Waking from hibernate", { sessionId, snapshotChars: hibernateSnapshotRef.current.length, viewportChars: hibernateViewportSnapshotRef.current.length, scrollbackChars: hibernateScrollbackSnapshotRef.current.length, pendingChars: hibernatePendingBufferRef.current.length, sessionConnected, }); void Promise.resolve(onWakeRef.current(getPayload, { sessionConnected })).then((accepted) => { if (accepted !== false) { clearHibernateState(); if (!resolveVisible()) { scheduleHibernate(); } } }); }; const applyVisibility = (visible: boolean) => { paneVisibleRef.current = visible; // Keep the write/recovery ref current even when Terminal memo used to skip // isVisible-only updates, and even when hibernate itself is disabled. isVisibleRef.current = visible; if (visible) { hiddenSinceRef.current = null; clearHibernateTimer(); tryWake(); return; } if (hibernateEnabled) { scheduleHibernate(); } }; if (!hibernateEnabled) { // Turning hibernate off must wake already soft-hidden / hibernated panes // immediately; waiting for the next tab select would leave the setting // ineffective for currently hidden sessions. clearHibernateTimer(); if (hibernatedRef.current || softHiddenRef.current) { tryWake(); } } applyVisibility(resolveVisible()); const unsubscribe = subscribePaneVisible(sessionId, () => { const visible = resolveVisible(); if (visible === paneVisibleRef.current) return; applyVisibility(visible); }); return () => { clearHibernateTimer(); unsubscribe(); }; }, [ fileTransferActive, getSessionConnectedRef, hasRuntimeRef, hibernateAlternateScreenRef, hibernateDelayMs, hibernateEnabled, hibernateContextScrollbackSnapshotRef, hibernateContextSnapshotRef, hibernateContextViewportSnapshotRef, hibernatePendingBufferRef, hibernateScrollbackSnapshotRef, hibernateSnapshotRef, hibernateViewportSnapshotRef, hibernatedRef, isSearchOpen, isVisible, isVisibleRef, sessionId, softHiddenRef, status, ]); }