diff --git a/packages/app/src/components/terminal-emulator.tsx b/packages/app/src/components/terminal-emulator.tsx index 4ba89f3d6..70624986d 100644 --- a/packages/app/src/components/terminal-emulator.tsx +++ b/packages/app/src/components/terminal-emulator.tsx @@ -29,6 +29,7 @@ interface TerminalEmulatorProps { onOutputChunkConsumed?: (sequence: number) => Promise | 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(null); const hostRef = useRef(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 (
(null); const [modifiers, setModifiers] = useState(EMPTY_MODIFIERS); const [focusRequestToken, setFocusRequestToken] = useState(0); + const [resizeRequestToken, setResizeRequestToken] = useState(0); const [hoveredTerminalId, setHoveredTerminalId] = useState(null); const [hoveredCloseTerminalId, setHoveredCloseTerminalId] = useState( 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} /> ) : ( diff --git a/packages/app/src/terminal/runtime/terminal-emulator-runtime.test.ts b/packages/app/src/terminal/runtime/terminal-emulator-runtime.test.ts index 138a00fc8..30f599dcb 100644 --- a/packages/app/src/terminal/runtime/terminal-emulator-runtime.test.ts +++ b/packages/app/src/terminal/runtime/terminal-emulator-runtime.test.ts @@ -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); + }); }); diff --git a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts index 1f78903a0..dfe09ed24 100644 --- a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts +++ b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts @@ -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; }