diff --git a/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts b/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts index 47216fbe..226e7f42 100644 --- a/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts @@ -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> => { + const success = terminalManager.resize(id, cols, rows); + return { success, data: { success } }; } ); diff --git a/apps/frontend/src/main/terminal/pty-manager.ts b/apps/frontend/src/main/terminal/pty-manager.ts index b1016a95..18fc7ab1 100644 --- a/apps/frontend/src/main/terminal/pty-manager.ts +++ b/apps/frontend/src/main/terminal/pty-manager.ts @@ -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; + } } /** diff --git a/apps/frontend/src/main/terminal/terminal-manager.ts b/apps/frontend/src/main/terminal/terminal-manager.ts index 69118a9d..a81f6e1e 100644 --- a/apps/frontend/src/main/terminal/terminal-manager.ts +++ b/apps/frontend/src/main/terminal/terminal-manager.ts @@ -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); } /** diff --git a/apps/frontend/src/preload/api/terminal-api.ts b/apps/frontend/src/preload/api/terminal-api.ts index 298ace44..67171091 100644 --- a/apps/frontend/src/preload/api/terminal-api.ts +++ b/apps/frontend/src/preload/api/terminal-api.ts @@ -33,7 +33,7 @@ export interface TerminalAPI { createTerminal: (options: TerminalCreateOptions) => Promise; destroyTerminal: (id: string) => Promise; sendTerminalInput: (id: string, data: string) => void; - resizeTerminal: (id: string, cols: number, rows: number) => void; + resizeTerminal: (id: string, cols: number, rows: number) => Promise>; invokeClaudeInTerminal: (id: string, cwd?: string) => void; generateTerminalName: (command: string, cwd?: string) => Promise>; 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> => + ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_RESIZE, id, cols, rows), invokeClaudeInTerminal: (id: string, cwd?: string): void => ipcRenderer.send(IPC_CHANNELS.TERMINAL_INVOKE_CLAUDE, id, cwd), diff --git a/apps/frontend/src/preload/index.ts b/apps/frontend/src/preload/index.ts index addd4fc0..ddf06002 100644 --- a/apps/frontend/src/preload/index.ts +++ b/apps/frontend/src/preload/index.ts @@ -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', +}); diff --git a/apps/frontend/src/renderer/components/Terminal.tsx b/apps/frontend/src/renderer/components/Terminal.tsx index 21348d42..76a9c733 100644 --- a/apps/frontend/src/renderer/components/Terminal.tsx +++ b/apps/frontend/src/renderer/components/Terminal.tsx @@ -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(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(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(undefined); + // Track when last auto-correction was performed to implement cooldown + const lastAutoCorrectionTimeRef = useRef(0); + // Track auto-correction frequency to detect potential deeper issues + // If corrections exceed threshold, it may indicate a persistent sync problem + const autoCorrectionCountRef = useRef(0); + const autoCorrectionWindowStartRef = useRef(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(0); + // Track post-creation dimension check timeout for cleanup + const postCreationTimeoutRef = useRef | null>(null); // Worktree dialog state const [showWorktreeDialog, setShowWorktreeDialog] = useState(false); @@ -108,18 +144,131 @@ export const Terminal = forwardRef(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(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(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(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(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(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(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(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(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(); diff --git a/apps/frontend/src/renderer/components/terminal/usePtyProcess.ts b/apps/frontend/src/renderer/components/terminal/usePtyProcess.ts index 2b47218e..533c3e94 100644 --- a/apps/frontend/src/renderer/components/terminal/usePtyProcess.ts +++ b/apps/frontend/src/renderer/components/terminal/usePtyProcess.ts @@ -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 diff --git a/apps/frontend/src/renderer/components/terminal/useXterm.ts b/apps/frontend/src/renderer/components/terminal/useXterm.ts index b71526c6..e8230285 100644 --- a/apps/frontend/src/renderer/components/terminal/useXterm.ts +++ b/apps/frontend/src/renderer/components/terminal/useXterm.ts @@ -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 { diff --git a/apps/frontend/src/renderer/lib/mocks/terminal-mock.ts b/apps/frontend/src/renderer/lib/mocks/terminal-mock.ts index ecd3488e..b91c4f74 100644 --- a/apps/frontend/src/renderer/lib/mocks/terminal-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/terminal-mock.ts @@ -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: () => { diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index ce48120b..231366bd 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -238,7 +238,7 @@ export interface ElectronAPI { createTerminal: (options: TerminalCreateOptions) => Promise; destroyTerminal: (id: string) => Promise; sendTerminalInput: (id: string, data: string) => void; - resizeTerminal: (id: string, cols: number, rows: number) => void; + resizeTerminal: (id: string, cols: number, rows: number) => Promise>; invokeClaudeInTerminal: (id: string, cwd?: string) => void; generateTerminalName: (command: string, cwd?: string) => Promise>; 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; } }