import React, { useContext, useRef } from 'react'; import { useTerminalViewport } from '../ink/hooks/use-terminal-viewport.js'; import { Box } from '../ink.js'; import { InVirtualListContext } from './messageActions.js'; type Props = { children: React.ReactNode; }; /** * Freezes children when they scroll above the terminal viewport (into scrollback). * * Any content change above the viewport forces log-update.ts into a full terminal * reset (it cannot partially update rows that have scrolled out). For content that * updates on a timer — spinners, elapsed counters — this produces a reset per tick. * * When offscreen, returns the same ReactElement reference that was cached during * the last visible render. React's reconciler bails on identical element refs, so * the subtree never re-renders, producing zero diff. * * The cache is one slot deep: the first re-render after scrolling back into view * picks up the live children. Content still updates normally while visible. */ export function OffscreenFreeze({ children }: Props): React.ReactNode { // React Compiler: reading cached.current in the return is the entire // freeze mechanism — memoizing this component would defeat it. Opt out. 'use no memo'; const inVirtualList = useContext(InVirtualListContext); const [ref, { isVisible }] = useTerminalViewport(); const cached = useRef(children); // Virtual list has no terminal scrollback — the ScrollBox clips inside the // viewport, so there's nothing to freeze. Freezing there also blocks // click-to-expand since useTerminalViewport's visibility calc can disagree // with the ScrollBox's virtual scroll position. if (isVisible || inVirtualList) { cached.current = children; } return {cached.current}; }