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}`); }