diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index 14f603b02..87baf954c 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -6,6 +6,8 @@ import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyl import { encodeTerminalKeyInput } from "@server/shared/terminal-key-input"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; +import { useAppVisible } from "@/hooks/use-app-visible"; +import { useStableEvent } from "@/hooks/use-stable-event"; import { hasPendingTerminalModifiers, normalizeTerminalTransportKey, @@ -86,6 +88,7 @@ export function TerminalPane({ isPaneFocused, }: TerminalPaneProps) { const isScreenFocused = useIsFocused(); + const isAppVisible = useAppVisible(); const { theme } = useUnistyles(); const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme.colors.terminal]); const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; @@ -143,6 +146,13 @@ export function TerminalPane({ requestTerminalFocus(); }, [isMobile, isPaneFocused, isScreenFocused, requestTerminalFocus, scopeKey, terminalId]); + useEffect(() => { + if (isPaneFocused && isScreenFocused && isAppVisible && terminalId) { + lastReportedSizeRef.current = null; + requestTerminalReflow(); + } + }, [isAppVisible, isPaneFocused, isScreenFocused, requestTerminalReflow, terminalId]); + const clearKeyboardRefitTimeouts = useCallback(() => { if (keyboardRefitTimeoutsRef.current.length === 0) { return; @@ -462,10 +472,10 @@ export function TerminalPane({ ], ); - const handleTerminalResize = useCallback( - async (input: { rows: number; cols: number }) => { + const handleTerminalResize = useStableEvent( + (input: { rows: number; cols: number }) => { const { rows, cols } = input; - if (!client || !terminalId || rows <= 0 || cols <= 0) { + if (!client || !terminalId || !isPaneFocused || !isScreenFocused || !isAppVisible || rows <= 0 || cols <= 0) { return; } const normalizedRows = Math.floor(rows); @@ -481,7 +491,6 @@ export function TerminalPane({ cols: normalizedCols, }); }, - [client, terminalId], ); const handleTerminalKey = useCallback( diff --git a/packages/app/src/hooks/use-app-visible.ts b/packages/app/src/hooks/use-app-visible.ts new file mode 100644 index 000000000..1d584121a --- /dev/null +++ b/packages/app/src/hooks/use-app-visible.ts @@ -0,0 +1,49 @@ +import { useEffect, useSyncExternalStore } from "react"; +import { AppState, Platform } from "react-native"; +import { getIsAppActivelyVisible } from "@/utils/app-visibility"; + +let current = getIsAppActivelyVisible(); +const listeners = new Set<() => void>(); + +function notify(): void { + const next = getIsAppActivelyVisible(); + if (next === current) { + return; + } + current = next; + for (const listener of listeners) { + listener(); + } +} + +function subscribe(listener: () => void): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +function getSnapshot(): boolean { + return current; +} + +export function useAppVisible(): boolean { + useEffect(() => { + const appStateSubscription = AppState.addEventListener("change", notify); + + if (Platform.OS === "web" && typeof document !== "undefined") { + document.addEventListener("visibilitychange", notify); + window.addEventListener("focus", notify); + window.addEventListener("blur", notify); + } + + return () => { + appStateSubscription.remove(); + if (Platform.OS === "web" && typeof document !== "undefined") { + document.removeEventListener("visibilitychange", notify); + window.removeEventListener("focus", notify); + window.removeEventListener("blur", notify); + } + }; + }, []); + + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +}