Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion apps/frontend/src/main/terminal/pty-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
24 changes: 20 additions & 4 deletions apps/frontend/src/renderer/components/terminal/useXterm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While the current logic works due to JavaScript's truthiness rules, explicitly converting the result to a boolean with !! would make the code more robust and the intent clearer. This ensures isClaudeActive is always a boolean and doesn't rely on the falsiness of undefined if terminal.isClaudeMode is false and terminal.pendingClaudeResume is undefined.

Suggested change
const isClaudeActive = terminal?.isClaudeMode || terminal?.pendingClaudeResume;
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}`);
}
Expand Down
Loading