fix(terminal): resolve text alignment issues on expand/minimize (#1650)

* fix(terminal): force PTY resize on mount and add auto-correction

Root cause: Architecture mismatch between PTY lifecycle (persists in main
process) and xterm lifecycle (destroyed/recreated on expand/minimize).
When terminal remounts, PTY keeps old dimensions but new xterm assumes
they match.

Changes:
- Force PTY resize on terminal mount/creation to ensure PTY matches xterm
- Add auto-correction to checkDimensionMismatch() with cooldown to fix
  any detected mismatches automatically
- Add validation and error handling to resizePty() in pty-manager
- Revert console.log to debugLog for production readiness

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(terminal): add IPC acknowledgment, platform-specific timing, and correction monitoring

- Change resizeTerminal from fire-and-forget to invoke/handle pattern
  so renderer gets confirmation of resize success/failure
- Add platform detection via contextBridge (isWindows, isMacOS, isLinux, isUnix)
- Use shorter grace periods on Unix (100ms) vs Windows (500ms) since
  Unix PTY resize is much faster than Windows ConPTY
- Track auto-correction frequency and log warning if >5 corrections
  occur per minute, indicating potential deeper sync issues
- Update all 4 resizeTerminal call sites to handle Promise and log failures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(terminal): prevent stale closures in dimension handling

- Read xterm dimensions from ref instead of React state to avoid stale closures
- Fix onCreated callback using potentially outdated ptyDimensions
- Fix expansion effect using old cols/rows after fit()
- Fix post-PTY creation timeout using stale dimensions
- Change warning threshold from > to >= for consistency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(terminal): only update lastPtyDimensionsRef after successful resize

Previously, lastPtyDimensionsRef was updated optimistically before the
async resizeTerminal() call completed. If the resize failed, the ref
would hold incorrect dimensions, causing future resize attempts with
the same target dimensions to be incorrectly skipped.

Now the ref is only updated after resizeTerminal() succeeds, and
reverted to previous dimensions on failure. This ensures dimension
mismatches aren't masked by failed resize operations.

Fixed in 4 locations:
- Auto-correction path
- onResize callback
- onCreated (PTY creation)
- performFit (expansion)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve CI failures from outdated develop branch

- Fix Ruff formatting in parallel_orchestrator_reviewer.py and pydantic_models.py
- Fix test_integration_phase4.py to register module in sys.modules before
  exec_module for Python 3.12+ dataclass compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR review feedback - race condition and platform detection

- Fix race condition in concurrent resize calls using sequence numbers
  to prevent stale dimension corruption when calls complete out-of-order
- Use consistent platform detection via os-detection.ts module instead
  of window.platform for codebase consistency
- Extract duplicated resizeTerminal promise handling to helper function
  resizePtyWithTracking() reducing code from 4 occurrences to 1
- Capture setTimeout ID for post-creation timeout to enable cleanup
- Add proper cleanup in unmount effect for the timeout ref

Addresses all blocking issues from Auto Claude PR Review:
- NEW-001 [HIGH]: Race condition in concurrent resize calls
- 47ffdb7e4a98 [HIGH]: Inconsistent platform detection
- eabaccf549e4 [MEDIUM]: Duplicated resizeTerminal promise handling
- 7821024a350c [LOW]: Uncleaned timeout in onCreated callback

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
VDT-91
2026-02-04 12:18:15 +01:00
committed by GitHub
parent 5f63daa3cc
commit f5a7e26d99
10 changed files with 291 additions and 33 deletions
@@ -55,10 +55,11 @@ export function registerTerminalHandlers(
}
);
ipcMain.on(
ipcMain.handle(
IPC_CHANNELS.TERMINAL_RESIZE,
(_, id: string, cols: number, rows: number) => {
terminalManager.resize(id, cols, rows);
async (_, id: string, cols: number, rows: number): Promise<IPCResult<{ success: boolean }>> => {
const success = terminalManager.resize(id, cols, rows);
return { success, data: { success } };
}
);
+24 -3
View File
@@ -168,6 +168,7 @@ export function spawnPtyProcess(
const shellArgs = isWindows() ? [] : ['-l'];
debugLog('[PtyManager] Spawning shell:', shell, shellArgs, '(preferred:', preferredTerminal || 'system', ', shellType:', shellType, ')');
debugLog('[PtyManager] PTY dimensions requested - cols:', cols, 'rows:', rows, 'cwd:', cwd || os.homedir());
// Create a clean environment without DEBUG to prevent Claude Code from
// enabling debug mode when the Electron app is run in development mode.
@@ -355,10 +356,30 @@ export function writeToPty(terminal: TerminalProcess, data: string): void {
}
/**
* Resize a PTY process
* Resize a PTY process with validation and error handling.
* @param terminal The terminal process to resize
* @param cols New column count
* @param rows New row count
* @returns true if resize was successful, false otherwise
*/
export function resizePty(terminal: TerminalProcess, cols: number, rows: number): void {
terminal.pty.resize(cols, rows);
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;
}
try {
const prevCols = terminal.pty.cols;
const prevRows = terminal.pty.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);
return true;
} catch (error) {
debugError('[PtyManager] Resize failed for terminal:', terminal.id, 'error:', error);
return false;
}
}
/**
@@ -137,12 +137,14 @@ export class TerminalManager {
/**
* Resize a terminal
* @returns true if resize was successful, false otherwise
*/
resize(id: string, cols: number, rows: number): void {
resize(id: string, cols: number, rows: number): boolean {
const terminal = this.terminals.get(id);
if (terminal) {
PtyManager.resizePty(terminal, cols, rows);
if (!terminal) {
return false;
}
return PtyManager.resizePty(terminal, cols, rows);
}
/**
@@ -33,7 +33,7 @@ export interface TerminalAPI {
createTerminal: (options: TerminalCreateOptions) => Promise<IPCResult>;
destroyTerminal: (id: string) => Promise<IPCResult>;
sendTerminalInput: (id: string, data: string) => void;
resizeTerminal: (id: string, cols: number, rows: number) => void;
resizeTerminal: (id: string, cols: number, rows: number) => Promise<IPCResult<{ success: boolean }>>;
invokeClaudeInTerminal: (id: string, cwd?: string) => void;
generateTerminalName: (command: string, cwd?: string) => Promise<IPCResult<string>>;
setTerminalTitle: (id: string, title: string) => void;
@@ -137,8 +137,8 @@ export const createTerminalAPI = (): TerminalAPI => ({
sendTerminalInput: (id: string, data: string): void =>
ipcRenderer.send(IPC_CHANNELS.TERMINAL_INPUT, id, data),
resizeTerminal: (id: string, cols: number, rows: number): void =>
ipcRenderer.send(IPC_CHANNELS.TERMINAL_RESIZE, id, cols, rows),
resizeTerminal: (id: string, cols: number, rows: number): Promise<IPCResult<{ success: boolean }>> =>
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_RESIZE, id, cols, rows),
invokeClaudeInTerminal: (id: string, cwd?: string): void =>
ipcRenderer.send(IPC_CHANNELS.TERMINAL_INVOKE_CLAUDE, id, cwd),
+8
View File
@@ -9,3 +9,11 @@ contextBridge.exposeInMainWorld('electronAPI', electronAPI);
// Expose debug flag for debug logging
contextBridge.exposeInMainWorld('DEBUG', process.env.DEBUG === 'true');
// Expose platform information for platform-specific behavior (e.g., PTY resize timing)
contextBridge.exposeInMainWorld('platform', {
isWindows: process.platform === 'win32',
isMacOS: process.platform === 'darwin',
isLinux: process.platform === 'linux',
isUnix: process.platform !== 'win32',
});
@@ -15,11 +15,30 @@ import { usePtyProcess } from './terminal/usePtyProcess';
import { useTerminalEvents } from './terminal/useTerminalEvents';
import { useAutoNaming } from './terminal/useAutoNaming';
import { useTerminalFileDrop } from './terminal/useTerminalFileDrop';
import { debugLog } from '../../shared/utils/debug-logger';
import { isWindows as checkIsWindows } from '../lib/os-detection';
// Minimum dimensions to prevent PTY creation with invalid sizes
const MIN_COLS = 10;
const MIN_ROWS = 3;
// Platform detection for platform-specific timing
// Windows ConPTY is slower than Unix PTY, so we need longer grace periods
const platformIsWindows = checkIsWindows();
// Threshold in milliseconds to allow for async PTY resize acknowledgment
// Mismatches within this window after a resize are expected and not logged as warnings
// Windows needs longer grace period due to slower ConPTY resize
const DIMENSION_MISMATCH_GRACE_PERIOD_MS = platformIsWindows ? 500 : 100;
// Cooldown between auto-corrections to prevent rapid-fire corrections
// Windows needs longer cooldown due to slower ConPTY operations
const AUTO_CORRECTION_COOLDOWN_MS = platformIsWindows ? 1000 : 300;
// Auto-correction frequency monitoring
const AUTO_CORRECTION_WARNING_THRESHOLD = 5; // Warn if > 5 corrections per minute
const AUTO_CORRECTION_WINDOW_MS = 60000; // 1 minute window
/**
* Handle interface exposed by Terminal component for external control.
* Used by parent components (e.g., SortableTerminalWrapper) to trigger operations
@@ -57,6 +76,23 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
// Track last sent PTY dimensions to prevent redundant resize calls
// This ensures terminal.resize() stays in sync with PTY dimensions
const lastPtyDimensionsRef = useRef<{ cols: number; rows: number } | null>(null);
// Track when the last resize was sent to PTY for grace period logic
// This prevents false positive mismatch warnings during async resize acknowledgment
const lastResizeTimeRef = useRef<number>(0);
// Track previous isExpanded state to detect actual expansion changes
// This prevents forcing PTY resize on initial mount (only on actual state changes)
const prevIsExpandedRef = useRef<boolean | undefined>(undefined);
// Track when last auto-correction was performed to implement cooldown
const lastAutoCorrectionTimeRef = useRef<number>(0);
// Track auto-correction frequency to detect potential deeper issues
// If corrections exceed threshold, it may indicate a persistent sync problem
const autoCorrectionCountRef = useRef<number>(0);
const autoCorrectionWindowStartRef = useRef<number>(Date.now());
// Sequence number for resize operations to prevent race conditions
// When concurrent resize calls complete out-of-order, only the latest result is applied
const resizeSequenceRef = useRef<number>(0);
// Track post-creation dimension check timeout for cleanup
const postCreationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Worktree dialog state
const [showWorktreeDialog, setShowWorktreeDialog] = useState(false);
@@ -108,18 +144,131 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
// Track when xterm dimensions are ready for PTY creation
const [readyDimensions, setReadyDimensions] = useState<{ cols: number; rows: number } | null>(null);
/**
* Helper function to resize PTY with proper dimension tracking and race condition prevention.
* Uses sequence numbers to ensure only the latest resize result updates the tracked dimensions.
* This prevents stale dimension corruption when concurrent resize calls complete out-of-order.
*
* @param cols - Target column count
* @param rows - Target row count
* @param context - Context string for debug logging (e.g., "onResize", "performFit")
*/
const resizePtyWithTracking = useCallback((cols: number, rows: number, context: string) => {
// Increment sequence number for this resize operation
const sequence = ++resizeSequenceRef.current;
lastResizeTimeRef.current = Date.now();
window.electronAPI.resizeTerminal(id, cols, rows).then((result) => {
// Only update dimensions if this is still the latest resize operation
// This prevents race conditions where an earlier failed call overwrites a later successful one
if (sequence !== resizeSequenceRef.current) {
debugLog(`[Terminal ${id}] ${context}: Ignoring stale resize result (sequence ${sequence} vs current ${resizeSequenceRef.current})`);
return;
}
if (result.success) {
lastPtyDimensionsRef.current = { cols, rows };
} else {
debugLog(`[Terminal ${id}] ${context} resize failed: ${result.error || 'unknown error'}`);
}
}).catch((error) => {
// Only log if this is still the latest operation
if (sequence === resizeSequenceRef.current) {
debugLog(`[Terminal ${id}] ${context} resize error: ${error}`);
}
});
}, [id]);
// Callback when xterm has measured valid dimensions
const handleDimensionsReady = useCallback((cols: number, rows: number) => {
// Only set dimensions if they're valid (above minimum thresholds)
if (cols >= MIN_COLS && rows >= MIN_ROWS) {
debugLog(`[Terminal ${id}] handleDimensionsReady: cols=${cols}, rows=${rows} - setting readyDimensions`);
setReadyDimensions({ cols, rows });
} else {
debugLog(`[Terminal ${id}] handleDimensionsReady: dimensions below minimum: cols=${cols} (min=${MIN_COLS}), rows=${rows} (min=${MIN_ROWS})`);
}
}, []);
}, [id]);
/**
* Check for dimension mismatch between xterm and PTY.
* Logs a warning if dimensions differ outside the grace period after a resize.
* This helps diagnose text alignment issues that can occur when xterm and PTY
* have different ideas about terminal dimensions.
*
* @param xtermCols - Current xterm column count
* @param xtermRows - Current xterm row count
* @param context - Optional context string for the log message (e.g., "after resize", "on fit")
* @param autoCorrect - If true, automatically correct mismatches by resizing PTY
*/
const checkDimensionMismatch = useCallback((
xtermCols: number,
xtermRows: number,
context?: string,
autoCorrect: boolean = false
) => {
const ptyDims = lastPtyDimensionsRef.current;
// Skip check if PTY hasn't been created yet (no dimensions to compare)
if (!ptyDims) {
return;
}
// Skip check if we're within the grace period after a resize
// This prevents false positives during async PTY resize acknowledgment
const timeSinceLastResize = Date.now() - lastResizeTimeRef.current;
if (timeSinceLastResize < DIMENSION_MISMATCH_GRACE_PERIOD_MS) {
return;
}
// Check for mismatch
const colsMismatch = xtermCols !== ptyDims.cols;
const rowsMismatch = xtermRows !== ptyDims.rows;
if (colsMismatch || rowsMismatch) {
const contextStr = context ? ` (${context})` : '';
debugLog(
`[Terminal ${id}] DIMENSION MISMATCH DETECTED${contextStr}: ` +
`xterm=(cols=${xtermCols}, rows=${xtermRows}) vs PTY=(cols=${ptyDims.cols}, rows=${ptyDims.rows}) - ` +
`delta=(cols=${xtermCols - ptyDims.cols}, rows=${xtermRows - ptyDims.rows})`
);
// Auto-correct if enabled, PTY is created, and cooldown has passed
const timeSinceAutoCorrect = Date.now() - lastAutoCorrectionTimeRef.current;
if (
autoCorrect &&
isCreatedRef.current &&
timeSinceAutoCorrect >= AUTO_CORRECTION_COOLDOWN_MS &&
xtermCols >= MIN_COLS &&
xtermRows >= MIN_ROWS
) {
// Track auto-correction frequency for monitoring
const now = Date.now();
if (now - autoCorrectionWindowStartRef.current >= AUTO_CORRECTION_WINDOW_MS) {
// Log warning if previous window had excessive corrections
if (autoCorrectionCountRef.current >= AUTO_CORRECTION_WARNING_THRESHOLD) {
debugLog(
`[Terminal ${id}] AUTO-CORRECTION WARNING: ${autoCorrectionCountRef.current} corrections ` +
`in last minute - this may indicate a persistent sync issue`
);
}
// Reset the window
autoCorrectionCountRef.current = 0;
autoCorrectionWindowStartRef.current = now;
}
autoCorrectionCountRef.current++;
debugLog(`[Terminal ${id}] AUTO-CORRECTING (#${autoCorrectionCountRef.current}): resizing PTY to ${xtermCols}x${xtermRows}`);
lastAutoCorrectionTimeRef.current = Date.now();
resizePtyWithTracking(xtermCols, xtermRows, 'AUTO-CORRECTION');
}
}
}, [id, resizePtyWithTracking]);
// Initialize xterm with command tracking
const {
terminalRef,
xtermRef: _xtermRef,
xtermRef,
fit,
write: _write, // Output now handled by useGlobalTerminalListeners
writeln,
@@ -150,9 +299,8 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
return;
}
// Update tracked dimensions and send resize to PTY
lastPtyDimensionsRef.current = { cols, rows };
window.electronAPI.resizeTerminal(id, cols, rows);
// Use helper to resize PTY with proper tracking and race condition prevention
resizePtyWithTracking(cols, rows, 'onResize');
},
onDimensionsReady: handleDimensionsReady,
});
@@ -167,15 +315,14 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
// This prevents creating PTY with default 80x24 when container is smaller
const ptyDimensions = useMemo(() => {
if (readyDimensions) {
debugLog(`[Terminal ${id}] ptyDimensions memo: using readyDimensions cols=${readyDimensions.cols}, rows=${readyDimensions.rows}`);
return readyDimensions;
}
// Fallback to current dimensions if they're valid
if (cols >= MIN_COLS && rows >= MIN_ROWS) {
return { cols, rows };
}
// Return null to prevent PTY creation until dimensions are ready
// Wait for actual measurement via onDimensionsReady callback
// Do NOT use current cols/rows as they may be initial defaults (80x24)
debugLog(`[Terminal ${id}] ptyDimensions memo: readyDimensions is null, returning null (skipCreation will be true)`);
return null;
}, [readyDimensions, cols, rows]);
}, [readyDimensions, id]);
// Create PTY process - only when we have valid dimensions
const { prepareForRecreate, resetForRecreate } = usePtyProcess({
@@ -190,10 +337,33 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
isRecreatingRef,
onCreated: () => {
isCreatedRef.current = true;
// Initialize PTY dimension tracking with creation dimensions
// This ensures the first resize check has a baseline to compare against
if (ptyDimensions) {
lastPtyDimensionsRef.current = { cols: ptyDimensions.cols, rows: ptyDimensions.rows };
// ALWAYS force PTY resize on creation/remount
// This ensures PTY matches xterm even if PTY existed before remount (expand/minimize)
// The root cause of text alignment issues is that when terminal remounts:
// 1. PTY persists with old dimensions (e.g., 80x20)
// 2. New xterm measures new container (e.g., 160x40)
// 3. Without this force resize, PTY never gets updated
// Read current dimensions from xterm ref to avoid stale closure values
const currentCols = xtermRef.current?.cols;
const currentRows = xtermRef.current?.rows;
if (currentCols !== undefined && currentRows !== undefined && currentCols >= MIN_COLS && currentRows >= MIN_ROWS) {
debugLog(`[Terminal ${id}] PTY created - forcing PTY resize to match xterm: cols=${currentCols}, rows=${currentRows}`);
// Use helper to resize PTY with proper tracking and race condition prevention
resizePtyWithTracking(currentCols, currentRows, 'PTY creation');
// Schedule initial dimension mismatch check after PTY creation
// This helps detect if xterm dimensions drifted during PTY setup
// Read fresh dimensions inside the timeout to avoid stale closure
// Store timeout ID for cleanup on unmount
postCreationTimeoutRef.current = setTimeout(() => {
const freshCols = xtermRef.current?.cols;
const freshRows = xtermRef.current?.rows;
if (freshCols !== undefined && freshRows !== undefined) {
checkDimensionMismatch(freshCols, freshRows, 'post-PTY creation');
}
}, DIMENSION_MISMATCH_GRACE_PERIOD_MS + 100);
} else {
debugLog(`[Terminal ${id}] PTY created - no valid dimensions available for tracking (cols=${currentCols}, rows=${currentRows})`);
}
// If there's a pending worktree config from a recreation attempt,
// sync it to main process now that the terminal exists.
@@ -217,6 +387,26 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
},
});
// Monitor for dimension mismatches between xterm and PTY
// This effect runs when xterm dimensions change and checks for mismatches
// after the grace period to help diagnose text alignment issues
// Auto-correction is enabled to automatically fix any detected mismatches
useEffect(() => {
// Only check if PTY has been created
if (!isCreatedRef.current) {
return;
}
// Schedule a mismatch check after the grace period
// This allows time for the PTY resize to be acknowledged
// Enable auto-correct to automatically fix any detected mismatches
const timeoutId = setTimeout(() => {
checkDimensionMismatch(cols, rows, 'periodic dimension sync check', true);
}, DIMENSION_MISMATCH_GRACE_PERIOD_MS + 100);
return () => clearTimeout(timeoutId);
}, [cols, rows, checkDimensionMismatch]);
// Handle terminal events (output is now handled globally via useGlobalTerminalListeners)
useTerminalEvents({
terminalId: id,
@@ -239,6 +429,13 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
// Uses transitionend event listener and RAF-based retry logic instead of fixed timeout
// for more reliable resizing after CSS transitions complete
useEffect(() => {
// Detect if this is an actual expansion state change vs initial mount
// Only force PTY resize on actual state changes to avoid resizing with invalid dimensions on mount
const isFirstMount = prevIsExpandedRef.current === undefined;
const expansionStateChanged = !isFirstMount && prevIsExpandedRef.current !== isExpanded;
debugLog(`[Terminal ${id}] Expansion effect: isExpanded=${isExpanded}, isFirstMount=${isFirstMount}, expansionStateChanged=${expansionStateChanged}, prevIsExpanded=${prevIsExpandedRef.current}`);
prevIsExpandedRef.current = isExpanded;
// RAF fallback for test environments where requestAnimationFrame may not be defined
const raf = typeof requestAnimationFrame !== 'undefined'
? requestAnimationFrame
@@ -273,9 +470,20 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
// fit() returns boolean indicating success (true if container had valid dimensions)
const success = fit();
debugLog(`[Terminal ${id}] performFit: fit returned success=${success}, expansionStateChanged=${expansionStateChanged}, isCreatedRef=${isCreatedRef.current}`);
if (success) {
fitSucceeded = true;
// Force PTY resize only on actual expansion state changes (not initial mount)
// This ensures PTY stays in sync even when xterm.onResize() doesn't fire
// Read fresh dimensions from xterm ref after fit() to avoid stale closure values
const freshCols = xtermRef.current?.cols;
const freshRows = xtermRef.current?.rows;
if (expansionStateChanged && isCreatedRef.current && freshCols !== undefined && freshRows !== undefined && freshCols >= MIN_COLS && freshRows >= MIN_ROWS) {
debugLog(`[Terminal ${id}] performFit: Forcing PTY resize to cols=${freshCols}, rows=${freshRows}`);
// Use helper to resize PTY with proper tracking and race condition prevention
resizePtyWithTracking(freshCols, freshRows, 'performFit');
}
} else if (retryCount < MAX_RETRIES) {
// Container not ready yet, retry after a short delay
retryCount++;
@@ -343,7 +551,7 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
container.parentElement?.removeEventListener('transitionend', handleTransitionEnd);
}
};
}, [isExpanded, fit]);
}, [isExpanded, fit, id, resizePtyWithTracking]);
// Trigger deferred Claude resume when terminal becomes active
// This ensures Claude sessions are only resumed when the user actually views the terminal,
@@ -390,6 +598,12 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
isMountedRef.current = false;
cleanupAutoNaming();
// Clear post-creation dimension check timeout to prevent operations on unmounted component
if (postCreationTimeoutRef.current !== null) {
clearTimeout(postCreationTimeoutRef.current);
postCreationTimeoutRef.current = null;
}
setTimeout(() => {
if (!isMountedRef.current) {
dispose();
@@ -124,7 +124,7 @@ export function usePtyProcess({
// Normal skip (not during recreation) - just return
if (skipCreation) {
debugLog(`[usePtyProcess] Skipping PTY creation for terminal: ${terminalId} - dimensions not ready`);
debugLog(`[usePtyProcess] Skipping PTY creation for terminal: ${terminalId} - dimensions not ready (skipCreation=true)`);
return;
}
if (isCreatingRef.current || isCreatedRef.current) {
@@ -140,7 +140,9 @@ export function usePtyProcess({
const alreadyRunning = terminalState?.status === 'running' || terminalState?.status === 'claude-active';
const isRestored = terminalState?.isRestored;
debugLog(`[usePtyProcess] Starting PTY creation for terminal: ${terminalId}, isRestored: ${isRestored}, status: ${terminalState?.status}, cols: ${cols}, rows: ${rows}`);
debugLog(`[usePtyProcess] Starting PTY creation for terminal: ${terminalId}`);
debugLog(`[usePtyProcess] Terminal ${terminalId} state: isRestored=${isRestored}, status=${terminalState?.status}`);
debugLog(`[usePtyProcess] Terminal ${terminalId} dimensions for PTY: cols=${cols}, rows=${rows}`);
// When recreating (e.g., worktree switching), reset status from 'exited' to 'idle'
// This allows proper recreation after deliberate terminal destruction
@@ -242,7 +242,7 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea
// Call onDimensionsReady once when we have valid dimensions
if (!dimensionsReadyCalledRef.current && cols > 0 && rows > 0) {
dimensionsReadyCalledRef.current = true;
debugLog(`[useXterm] Dimensions ready for terminal: ${terminalId}, cols: ${cols}, rows: ${rows}`);
debugLog(`[useXterm] Dimensions ready for terminal: ${terminalId}, cols: ${cols}, rows: ${rows}, containerWidth: ${rect.width}, containerHeight: ${rect.height}`);
onDimensionsReady?.(cols, rows);
}
} else {
@@ -17,8 +17,9 @@ export const terminalMock = {
console.warn('[Browser Mock] sendTerminalInput called');
},
resizeTerminal: () => {
resizeTerminal: async () => {
console.warn('[Browser Mock] resizeTerminal called');
return { success: true, data: { success: true } };
},
invokeClaudeInTerminal: () => {
+10 -1
View File
@@ -238,7 +238,7 @@ export interface ElectronAPI {
createTerminal: (options: TerminalCreateOptions) => Promise<IPCResult>;
destroyTerminal: (id: string) => Promise<IPCResult>;
sendTerminalInput: (id: string, data: string) => void;
resizeTerminal: (id: string, cols: number, rows: number) => void;
resizeTerminal: (id: string, cols: number, rows: number) => Promise<IPCResult<{ success: boolean }>>;
invokeClaudeInTerminal: (id: string, cwd?: string) => void;
generateTerminalName: (command: string, cwd?: string) => Promise<IPCResult<string>>;
setTerminalTitle: (id: string, title: string) => void;
@@ -916,9 +916,18 @@ export interface ElectronAPI {
queue: import('../../preload/api/queue-api').QueueAPI;
}
/** Platform information exposed via contextBridge for platform-specific behavior */
export interface PlatformInfo {
isWindows: boolean;
isMacOS: boolean;
isLinux: boolean;
isUnix: boolean;
}
declare global {
interface Window {
electronAPI: ElectronAPI;
DEBUG: boolean;
platform?: PlatformInfo;
}
}