From 2cf84a1fc575bf30645140aaf7ce52be10a06363 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 22 Jan 2026 15:45:15 -0800 Subject: [PATCH] fix(terminal): sync PTY dimensions after restore to prevent autocomplete issues When a terminal is attached/restored, createOrAttach was called with dimensions from an initial fitAddon.fit() that ran before the container was fully laid out by CSS. This caused a ~140ms window where the PTY had incorrect dimensions, leading to: - zsh autocomplete overwriting lines - Weird positioning when reattaching terminals The fix sends a resize to the PTY immediately after fit() completes in scheduleFitAndScroll, ensuring dimensions are synced before any user interaction. Also fixes scroll-to-bottom timing by ensuring all pending xterm writes are processed before scrolling. xterm.write() is async and buffers writes, so scrollToBottom() called immediately might not see all content. Also fixes unexpected scroll-to-bottom when switching tabs - now only scrolls to bottom for new sessions, not reattached ones, preserving the user's scroll position. - Add onResize callback to useTerminalRestore hook - Call onResize after fitAddon.fit() in scheduleFitAndScroll - Use xterm.write('', callback) to flush writes before scrolling - Only scroll to bottom for new sessions (result.isNew) --- .../TabsContent/Terminal/Terminal.tsx | 1 + .../Terminal/hooks/useTerminalRestore.ts | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx index cdc27f69ad1..569fa7e9de4 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx @@ -189,6 +189,7 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { onErrorEvent: (event, xterm) => handleStreamErrorRef.current(event, xterm), onDisconnectEvent: (reason) => setConnectionError(reason || "Connection to terminal daemon lost"), + onResize: (cols, rows) => resizeRef.current({ paneId, cols, rows }), }); // Cold restore handling diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalRestore.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalRestore.ts index e42ed27d955..b793124382a 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalRestore.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalRestore.ts @@ -21,6 +21,8 @@ export interface UseTerminalRestoreOptions { xterm: XTerm, ) => void; onDisconnectEvent: (reason: string | undefined) => void; + /** Callback to send resize to PTY after fit() - ensures PTY dimensions match xterm */ + onResize: (cols: number, rows: number) => void; } export interface UseTerminalRestoreReturn { @@ -54,6 +56,7 @@ export function useTerminalRestore({ onExitEvent, onErrorEvent, onDisconnectEvent, + onResize, }: UseTerminalRestoreOptions): UseTerminalRestoreReturn { // Gate streaming until initial state restoration is applied const isStreamReadyRef = useRef(false); @@ -73,6 +76,8 @@ export function useTerminalRestore({ onErrorEventRef.current = onErrorEvent; const onDisconnectEventRef = useRef(onDisconnectEvent); onDisconnectEventRef.current = onDisconnectEvent; + const onResizeRef = useRef(onResize); + onResizeRef.current = onResize; const flushPendingEvents = useCallback(() => { const xterm = xtermRef.current; @@ -119,7 +124,19 @@ export function useTerminalRestore({ if (xtermRef.current !== xterm) return; if (restoreSequenceRef.current !== restoreSequence) return; fitAddon.fit(); - scrollToBottom(xterm); + // Send resize to PTY after fit() to ensure dimensions are synced. + // This fixes the race condition where createOrAttach uses stale dimensions + // from before the container was fully laid out. + onResizeRef.current(xterm.cols, xterm.rows); + // Only scroll to bottom for NEW sessions. For reattached sessions, + // the snapshot already positions the viewport correctly and we should + // not override the user's scroll position. + if (result.isNew) { + // Write empty string with callback to ensure all pending writes are + // processed before scrolling. xterm.write() is async and buffers writes, + // so scrollToBottom() called immediately might not see all content. + xterm.write("", () => scrollToBottom(xterm)); + } }); };