From 7f6050f5ba9477995834287009812266323cc9da Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Sat, 14 Feb 2026 21:03:33 +0100 Subject: [PATCH] fix(terminal): resolve blank terminals after project switch Force SIGWINCH on same-dimension resize and skip stale buffer replay for Claude-mode terminals during project switch remount. Two compounding issues caused blank terminals when switching projects: 1. On macOS/Linux, ioctl(TIOCSWINSZ) only sends SIGWINCH when dimensions actually change. After project switch, PTY persists with old dimensions and the terminal remounts at the same size, so TUI apps never get SIGWINCH and never redraw. Fix: resize to (cols-1, rows) first, then to (cols, rows) to force the signal. 2. Buffer replay concatenated serialized xterm state with raw PTY output accumulated during the unmount period, producing garbled display. Fix: skip buffer replay for Claude-mode terminals on project switch remount (the forced SIGWINCH makes Claude Code redraw its full TUI). Initial restore still replays the buffer as a loading preview. Co-Authored-By: Claude Opus 4.6 --- .../frontend/src/main/terminal/pty-manager.ts | 12 +++++++++- .../renderer/components/terminal/useXterm.ts | 24 +++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/main/terminal/pty-manager.ts b/apps/frontend/src/main/terminal/pty-manager.ts index 4a188e341b..c43e44f37f 100644 --- a/apps/frontend/src/main/terminal/pty-manager.ts +++ b/apps/frontend/src/main/terminal/pty-manager.ts @@ -366,7 +366,6 @@ export function writeToPty(terminal: TerminalProcess, data: string): void { * @returns true if resize was successful, false otherwise */ export function resizePty(terminal: TerminalProcess, cols: number, rows: number): boolean { - // Validate dimensions if (cols <= 0 || rows <= 0 || !Number.isFinite(cols) || !Number.isFinite(rows)) { debugError('[PtyManager] Invalid resize dimensions - terminal:', terminal.id, 'cols:', cols, 'rows:', rows); return false; @@ -375,6 +374,17 @@ export function resizePty(terminal: TerminalProcess, cols: number, rows: number) try { const prevCols = terminal.pty.cols; const prevRows = terminal.pty.rows; + + // If dimensions are unchanged, force SIGWINCH via a resize cycle. + // On macOS/Linux, ioctl(TIOCSWINSZ) only sends SIGWINCH when size actually + // changes. This matters after project switch: PTY persists with old dimensions, + // terminal remounts at same size, TUI apps (Claude Code) never get SIGWINCH + // and never redraw — leaving the terminal blank. + if (prevCols === cols && prevRows === rows) { + debugLog('[PtyManager] Same-dimension resize detected, forcing SIGWINCH cycle for terminal:', terminal.id); + terminal.pty.resize(Math.max(1, cols - 1), rows); + } + debugLog('[PtyManager] Resizing PTY - terminal:', terminal.id, 'from:', prevCols, 'x', prevRows, 'to:', cols, 'x', rows); terminal.pty.resize(cols, rows); debugLog('[PtyManager] PTY resized - actual dimensions now:', terminal.pty.cols, 'x', terminal.pty.rows); diff --git a/apps/frontend/src/renderer/components/terminal/useXterm.ts b/apps/frontend/src/renderer/components/terminal/useXterm.ts index f2382c7dfd..9eb1964785 100644 --- a/apps/frontend/src/renderer/components/terminal/useXterm.ts +++ b/apps/frontend/src/renderer/components/terminal/useXterm.ts @@ -4,7 +4,7 @@ import { FitAddon } from '@xterm/addon-fit'; import { WebLinksAddon } from '@xterm/addon-web-links'; import { SerializeAddon } from '@xterm/addon-serialize'; import { terminalBufferManager } from '../../lib/terminal-buffer-manager'; -import { registerOutputCallback, unregisterOutputCallback } from '../../stores/terminal-store'; +import { registerOutputCallback, unregisterOutputCallback, useTerminalStore } from '../../stores/terminal-store'; import { useTerminalFontSettingsStore } from '../../stores/terminal-font-settings-store'; import { isWindows as checkIsWindows, isLinux as checkIsLinux } from '../../lib/os-detection'; import { debounce } from '../../lib/debounce'; @@ -279,9 +279,25 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea // Use atomic getAndClear to prevent race condition where new output could arrive between get() and clear() const bufferedOutput = terminalBufferManager.getAndClear(terminalId); if (bufferedOutput && bufferedOutput.length > 0) { - debugLog(`[useXterm] Replaying buffered output for terminal: ${terminalId}, buffer size: ${bufferedOutput.length} chars`); - xterm.write(bufferedOutput); - debugLog(`[useXterm] Buffer replay complete and cleared for terminal: ${terminalId}`); + // For Claude-mode terminals that are NOT being restored for the first time + // (i.e., project switch remount), skip buffer replay. + // Reason: the buffer contains serialized state + accumulated raw PTY output + // from the TUI during the unmount period. This concatenation creates garbled + // display. The forced SIGWINCH (from pty-manager) will make Claude Code redraw + // its full TUI properly. + // For initial restore (isRestored=true), we DO replay to show the saved state + // as a loading preview while claude --continue starts. + const terminal = useTerminalStore.getState().terminals.find(t => t.id === terminalId); + const isClaudeActive = terminal?.isClaudeMode || terminal?.pendingClaudeResume; + const isInitialRestore = terminal?.isRestored === true; + + if (isClaudeActive && !isInitialRestore) { + debugLog(`[useXterm] Skipping buffer replay for Claude-mode terminal on project switch remount: ${terminalId}`); + } else { + debugLog(`[useXterm] Replaying buffered output for terminal: ${terminalId}, buffer size: ${bufferedOutput.length} chars`); + xterm.write(bufferedOutput); + debugLog(`[useXterm] Buffer replay complete and cleared for terminal: ${terminalId}`); + } } else { debugLog(`[useXterm] No buffered output to replay for terminal: ${terminalId}`); }