Fix terminal resize after splitting panes (#1249)

This commit is contained in:
Mohamed Boudra
2026-05-31 15:42:39 +08:00
committed by GitHub
parent d1f37b5d72
commit 67133e3ebc
10 changed files with 250 additions and 103 deletions
@@ -50,7 +50,8 @@ interface TerminalEmulatorProps {
onSwipeRight?: () => void;
initialSnapshot?: TerminalState | null;
onInput?: (data: string) => Promise<void> | void;
onResize?: (input: { rows: number; cols: number }) => Promise<void> | void;
onFocus?: () => Promise<void> | void;
onResize?: (input: { rows: number; cols: number; shouldClaim: boolean }) => Promise<void> | void;
onTerminalKey?: (input: {
key: string;
ctrl: boolean;
@@ -89,7 +90,7 @@ type BridgeInboundMessage =
| { type: "renderSnapshot"; streamKey: string; state: TerminalState | null }
| { type: "clear"; streamKey: string }
| { type: "focus"; streamKey: string; forceRefocus?: boolean }
| { type: "resize"; streamKey: string }
| { type: "resize"; streamKey: string; shouldClaim?: boolean }
| { type: "setTheme"; streamKey: string; theme: ITheme }
| { type: "setScrollback"; streamKey: string; lines: number }
| { type: "setPendingModifiers"; streamKey: string; pendingModifiers: PendingTerminalModifiers }
@@ -105,7 +106,7 @@ type BridgeOutboundMessage =
| { type: "bridgeReady" }
| { type: "rendererReady"; streamKey: string; isReady: boolean }
| { type: "input"; streamKey: string; data: string }
| { type: "resize"; streamKey: string; rows: number; cols: number }
| { type: "resize"; streamKey: string; rows: number; cols: number; shouldClaim?: boolean }
| {
type: "terminalKey";
streamKey: string;
@@ -188,6 +189,7 @@ export default function TerminalEmulator({
onSwipeRight,
initialSnapshot = null,
onInput,
onFocus,
onResize,
onTerminalKey,
onPendingModifiersConsumed,
@@ -229,6 +231,7 @@ export default function TerminalEmulator({
};
const callbacksRef = useRef({
onInput,
onFocus,
onResize,
onTerminalKey,
onPendingModifiersConsumed,
@@ -241,6 +244,7 @@ export default function TerminalEmulator({
});
callbacksRef.current = {
onInput,
onFocus,
onResize,
onTerminalKey,
onPendingModifiersConsumed,
@@ -401,14 +405,14 @@ export default function TerminalEmulator({
useEffect(() => {
if (focusRequestToken <= 0) return;
sendToWebView({ type: "resize", streamKey });
sendToWebView({ type: "resize", streamKey, shouldClaim: true });
sendToWebView({ type: "focus", streamKey });
webViewRef.current?.requestFocus();
}, [focusRequestToken, sendToWebView, streamKey]);
useEffect(() => {
if (resizeRequestToken <= 0) return;
sendToWebView({ type: "resize", streamKey });
sendToWebView({ type: "resize", streamKey, shouldClaim: true });
}, [resizeRequestToken, sendToWebView, streamKey]);
useEffect(() => {
@@ -495,7 +499,11 @@ export default function TerminalEmulator({
callbacksRef.current.onInput?.(message.data);
break;
case "resize":
callbacksRef.current.onResize?.({ rows: message.rows, cols: message.cols });
callbacksRef.current.onResize?.({
rows: message.rows,
cols: message.cols,
shouldClaim: message.shouldClaim !== false,
});
break;
case "terminalKey":
callbacksRef.current.onTerminalKey?.({
@@ -592,6 +600,7 @@ export default function TerminalEmulator({
return;
}
webViewRef.current?.requestFocus();
callbacksRef.current.onFocus?.();
sendToWebView({ type: "focus", streamKey, forceRefocus: true });
}, [sendToWebView, streamKey]);
@@ -139,7 +139,8 @@ interface TerminalEmulatorProps {
onSwipeRight?: () => void;
initialSnapshot?: TerminalState | null;
onInput?: (data: string) => Promise<void> | void;
onResize?: (input: { rows: number; cols: number }) => Promise<void> | void;
onFocus?: () => Promise<void> | void;
onResize?: (input: { rows: number; cols: number; shouldClaim: boolean }) => Promise<void> | void;
onTerminalKey?: (input: {
key: string;
ctrl: boolean;
@@ -219,6 +220,7 @@ export default function TerminalEmulator({
onSwipeRight,
initialSnapshot = null,
onInput,
onFocus,
onResize,
onTerminalKey,
onPendingModifiersConsumed,
@@ -527,7 +529,7 @@ export default function TerminalEmulator({
if (focusRequestToken <= 0) {
return () => {};
}
runtimeRef.current?.resize({ force: true });
runtimeRef.current?.resize({ force: true, shouldClaim: true });
return focusWithRetries({
focus: () => {
runtimeRef.current?.focus();
@@ -547,7 +549,7 @@ export default function TerminalEmulator({
if (resizeRequestToken <= 0) {
return;
}
runtimeRef.current?.resize({ force: true });
runtimeRef.current?.resize({ force: true, shouldClaim: true });
}, [resizeRequestToken]);
useEffect(() => {
@@ -742,8 +744,9 @@ export default function TerminalEmulator({
}, []);
const handleRootPointerDown = useCallback(() => {
onFocus?.();
runtimeRef.current?.focus();
}, []);
}, [onFocus]);
const handleRootContextMenu = useCallback(
(event: ReactMouseEvent) => {
+58 -52
View File
@@ -214,6 +214,7 @@ export function TerminalPane({
const pendingTerminalInputRef = useRef<PendingTerminalInput[]>([]);
const keyboardRefitTimeoutsRef = useRef<Array<ReturnType<typeof setTimeout>>>([]);
const lastAutoFocusKeyRef = useRef<string | null>(null);
const lastPaneFocusResizeKeyRef = useRef<string | null>(null);
const initialSnapshot = workspaceTerminalSession.snapshots.get({ terminalId });
useEffect(() => {
@@ -252,7 +253,7 @@ export function TerminalPane({
);
useEffect(() => {
if (isMobile || !isWorkspaceFocused || !isPaneFocused || !terminalId) {
if (isMobile || !isPaneFocused || !terminalId) {
lastAutoFocusKeyRef.current = null;
return;
}
@@ -263,15 +264,36 @@ export function TerminalPane({
}
lastAutoFocusKeyRef.current = nextFocusKey;
if (!isWorkspaceFocused) {
return;
}
requestTerminalFocus();
}, [isMobile, isPaneFocused, isWorkspaceFocused, requestTerminalFocus, scopeKey, terminalId]);
useEffect(() => {
if (isPaneFocused && isWorkspaceFocused && isAppVisible && terminalId) {
lastSentTerminalSizeRef.current = null;
requestTerminalReflow();
if (!isPaneFocused || !terminalId) {
lastPaneFocusResizeKeyRef.current = null;
return;
}
}, [isAppVisible, isPaneFocused, isWorkspaceFocused, requestTerminalReflow, terminalId]);
const focusResizeKey = `${scopeKey}:${terminalId}`;
if (lastPaneFocusResizeKeyRef.current === focusResizeKey) {
return;
}
lastPaneFocusResizeKeyRef.current = focusResizeKey;
if (!isWorkspaceFocused) {
return;
}
lastSentTerminalSizeRef.current = null;
requestTerminalReflow();
}, [isPaneFocused, isWorkspaceFocused, requestTerminalReflow, scopeKey, terminalId]);
const handleTerminalFocus = useCallback(() => {
lastSentTerminalSizeRef.current = null;
requestTerminalReflow();
}, [requestTerminalReflow]);
const clearKeyboardRefitTimeouts = useCallback(() => {
if (keyboardRefitTimeoutsRef.current.length === 0) {
@@ -308,25 +330,6 @@ export function TerminalPane({
[pulseKeyboardRefits],
);
useEffect(() => {
if (!isWorkspaceFocused || !terminalId) {
return;
}
// Focus transitions can temporarily report stale dimensions.
// Pulse forced refits so xterm fills the pane when returning to a terminal.
const timeoutHandles = TERMINAL_REFIT_DELAYS_MS.map((delayMs) =>
setTimeout(() => {
requestTerminalReflow();
}, delayMs),
);
return () => {
for (const handle of timeoutHandles) {
clearTimeout(handle);
}
};
}, [isWorkspaceFocused, requestTerminalReflow, terminalId]);
useEffect(() => {
if (!client || !isConnected || !isWorkspaceFocused) {
return;
@@ -375,7 +378,7 @@ export function TerminalPane({
const controller = new TerminalStreamController({
client,
getPreferredSize: () => measuredTerminalSizeRef.current,
getPreferredSize: () => lastSentTerminalSizeRef.current,
onOutput: ({ terminalId: outputTerminalId, data }) => {
if (!isWorkspaceFocused || terminalIdRef.current !== outputTerminalId) {
return;
@@ -605,33 +608,35 @@ export function TerminalPane({
],
);
const handleTerminalResize = useStableEvent((input: { rows: number; cols: number }) => {
const { rows, cols } = input;
if (rows <= 0 || cols <= 0) {
return;
}
const normalizedRows = Math.floor(rows);
const normalizedCols = Math.floor(cols);
const nextSize = { rows: normalizedRows, cols: normalizedCols };
measuredTerminalSizeRef.current = nextSize;
if (!client || !terminalId || !isPaneFocused || !isWorkspaceFocused || !isAppVisible) {
return;
}
const previousSent = lastSentTerminalSizeRef.current;
if (
previousSent &&
previousSent.rows === normalizedRows &&
previousSent.cols === normalizedCols
) {
return;
}
lastSentTerminalSizeRef.current = nextSize;
client.sendTerminalInput(terminalId, {
type: "resize",
rows: normalizedRows,
cols: normalizedCols,
});
});
const handleTerminalResize = useStableEvent(
(input: { rows: number; cols: number; shouldClaim: boolean }) => {
const { rows, cols } = input;
if (rows <= 0 || cols <= 0) {
return;
}
const normalizedRows = Math.floor(rows);
const normalizedCols = Math.floor(cols);
const nextSize = { rows: normalizedRows, cols: normalizedCols };
measuredTerminalSizeRef.current = nextSize;
if (!input.shouldClaim || !client || !terminalId || !isWorkspaceFocused || !isAppVisible) {
return;
}
const previousSent = lastSentTerminalSizeRef.current;
if (
previousSent &&
previousSent.rows === normalizedRows &&
previousSent.cols === normalizedCols
) {
return;
}
lastSentTerminalSizeRef.current = nextSize;
client.sendTerminalInput(terminalId, {
type: "resize",
rows: normalizedRows,
cols: normalizedCols,
});
},
);
const handleTerminalKey = useCallback(
async (input: { key: string; ctrl: boolean; shift: boolean; alt: boolean; meta: boolean }) => {
@@ -764,6 +769,7 @@ export function TerminalPane({
onSwipeRight={handleSwipeRight}
onSwipeLeft={handleSwipeLeft}
onInput={handleTerminalData}
onFocus={handleTerminalFocus}
onResize={handleTerminalResize}
onTerminalKey={handleTerminalKey}
onInputModeChange={handleInputModeChange}