Merge pull request #45 from getpaseo/fix/terminal-resize-agent-switch-clean

fix: terminal shrinks after switching agents
This commit is contained in:
Mohamed Boudra
2026-02-18 09:58:59 +07:00
committed by GitHub
4 changed files with 58 additions and 0 deletions

View File

@@ -29,6 +29,7 @@ interface TerminalEmulatorProps {
onOutputChunkConsumed?: (sequence: number) => Promise<void> | void;
pendingModifiers?: PendingTerminalModifiers;
focusRequestToken?: number;
resizeRequestToken?: number;
}
declare global {
@@ -51,6 +52,7 @@ export default function TerminalEmulator({
onOutputChunkConsumed,
pendingModifiers = { ctrl: false, shift: false, alt: false },
focusRequestToken = 0,
resizeRequestToken = 0,
}: TerminalEmulatorProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
const hostRef = useRef<HTMLDivElement | null>(null);
@@ -142,6 +144,13 @@ export default function TerminalEmulator({
runtimeRef.current?.focus();
}, [focusRequestToken]);
useEffect(() => {
if (resizeRequestToken <= 0) {
return;
}
runtimeRef.current?.resize({ force: true });
}, [resizeRequestToken]);
return (
<div
ref={rootRef}

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useFocusEffect } from "@react-navigation/native";
import {
ActivityIndicator,
Pressable,
@@ -45,6 +46,7 @@ interface TerminalPaneProps {
const MAX_OUTPUT_CHARS = 200_000;
const TERMINAL_TAB_MAX_WIDTH = 220;
const TERMINAL_REFIT_DELAYS_MS = [0, 48, 144, 320];
const MODIFIER_LABELS = {
ctrl: "Ctrl",
@@ -159,6 +161,7 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) {
const [streamError, setStreamError] = useState<string | null>(null);
const [modifiers, setModifiers] = useState<ModifierState>(EMPTY_MODIFIERS);
const [focusRequestToken, setFocusRequestToken] = useState(0);
const [resizeRequestToken, setResizeRequestToken] = useState(0);
const [hoveredTerminalId, setHoveredTerminalId] = useState<string | null>(null);
const [hoveredCloseTerminalId, setHoveredCloseTerminalId] = useState<string | null>(
null
@@ -253,6 +256,30 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) {
const requestTerminalFocus = useCallback(() => {
setFocusRequestToken((current) => current + 1);
}, []);
const requestTerminalReflow = useCallback(() => {
setResizeRequestToken((current) => current + 1);
}, []);
useFocusEffect(
useCallback(() => {
if (!selectedTerminalId) {
return;
}
// Navigation transitions can temporarily report stale dimensions.
// Pulse forced refits so xterm fills the pane when returning to an agent.
const timeoutHandles = TERMINAL_REFIT_DELAYS_MS.map((delayMs) =>
setTimeout(() => {
requestTerminalReflow();
}, delayMs)
);
return () => {
for (const handle of timeoutHandles) {
clearTimeout(handle);
}
};
}, [requestTerminalReflow, selectedTerminalId])
);
const terminalsQuery = useQuery({
queryKey: terminalsQueryKey,
@@ -938,6 +965,7 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) {
onOutputChunkConsumed={handleOutputChunkConsumed}
pendingModifiers={modifiers}
focusRequestToken={focusRequestToken}
resizeRequestToken={resizeRequestToken}
/>
</View>
) : (

View File

@@ -157,4 +157,18 @@ describe("terminal-emulator-runtime", () => {
expect(onCommittedA).toHaveBeenCalledTimes(1);
expect(onCommittedB).toHaveBeenCalledTimes(1);
});
it("forces a refit when resize is requested", () => {
const runtime = new TerminalEmulatorRuntime();
const fitAndEmitResize = vi.fn();
(runtime as unknown as { fitAndEmitResize: (force: boolean) => void }).fitAndEmitResize =
fitAndEmitResize;
runtime.resize();
runtime.resize({ force: true });
expect(fitAndEmitResize).toHaveBeenNthCalledWith(1, false);
expect(fitAndEmitResize).toHaveBeenNthCalledWith(2, true);
});
});

View File

@@ -76,6 +76,7 @@ export class TerminalEmulatorRuntime {
};
private terminal: Terminal | null = null;
private fitAddon: FitAddon | null = null;
private fitAndEmitResize: ((force: boolean) => void) | null = null;
private lastSize: { rows: number; cols: number } | null = null;
private cleanup: (() => void) | null = null;
private outputOperations: TerminalOutputOperation[] = [];
@@ -168,6 +169,7 @@ export class TerminalEmulatorRuntime {
cols: nextCols,
});
};
this.fitAndEmitResize = fitAndEmitResize;
fitAndEmitResize(true);
@@ -383,6 +385,10 @@ export class TerminalEmulatorRuntime {
this.processOutputQueue();
}
resize(input?: { force?: boolean }): void {
this.fitAndEmitResize?.(input?.force ?? false);
}
focus(): void {
this.terminal?.focus();
}
@@ -413,6 +419,7 @@ export class TerminalEmulatorRuntime {
}
this.terminal = null;
this.fitAddon = null;
this.fitAndEmitResize = null;
this.lastSize = null;
}