fix(terminal): only send resize from the focused client
Multiple clients viewing the same terminal would fight over PTY size because every client sent resizes independently. Gate resize sends on three focus levels: pane focus (split panes), screen focus (navigation), and app visibility (browser window / mobile foreground). When a client regains focus, clear the last reported size and trigger a reflow to reclaim the PTY dimensions.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user