Fix Terminal UI Rendering Issues (#1514)
* auto-claude: subtask-1-1 - Add PROMPT_EOL_MARK='' to PTY spawn environment Add PROMPT_EOL_MARK='' environment variable to suppress zsh's partial line indicator (%) that appears when command output doesn't end with a newline. This prevents rendering artifacts in the terminal UI. * auto-claude: subtask-2-1 - Increase resize debounce from 100ms to 200ms * auto-claude: subtask-2-2 - Add PTY dimension sync validation in Terminal.tsx - Add lastPtyDimensionsRef to track last sent PTY dimensions - Validate dimensions are within acceptable range before sending resize - Skip redundant resize calls when dimensions haven't changed - Reset dimension tracking during worktree switching - Initialize dimension tracking when PTY is created This prevents race conditions from rapid resize events and ensures terminal.resize() stays in sync with PTY dimensions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-3-1 - Add Safari browser detection function to webgl-utils * auto-claude: subtask-3-2 - Update webgl-context-manager.ts to skip WebGL on Safari - Import isSafari from webgl-utils - Check for Safari browser in constructor - Disable WebGL and force Canvas renderer fallback on Safari - Add informative log message when Safari is detected Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-4-1 - Add staggered terminal restoration in TerminalGrid Add 75ms delay between adding each restored terminal to prevent race conditions when multiple terminals initialize and measure dimensions simultaneously during session restoration from history. * auto-claude: subtask-4-2 - Add terminal refit trigger after grid layout stabi Add terminal-refit-all event dispatch after session restoration loop completes to force dimension recalculation once all terminals are added. This ensures correct terminal dimensions after grid layout stabilizes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: consolidate Safari detection logic in webgl-utils Replace inline Safari user-agent check with isSafari() function to eliminate duplicate detection logic. This removes maintenance risk where Safari detection was inconsistent - getMaxWebGLContexts() was using simple userAgent.includes('safari') while isSafari() correctly excludes Chrome/Chromium browsers. The bug was latent due to Chrome being checked first in the if-else chain, but this consolidation prevents future issues if conditions are reordered or modified. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,6 +161,9 @@ export function spawnPtyProcess(
|
||||
...profileEnv,
|
||||
TERM: 'xterm-256color',
|
||||
COLORTERM: 'truecolor',
|
||||
// Suppress zsh's partial line indicator (%) that appears when output
|
||||
// doesn't end with a newline. This prevents rendering artifacts in the terminal.
|
||||
PROMPT_EOL_MARK: '',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
|
||||
// This fixes a race condition where IPC calls to set worktree config happen before
|
||||
// the terminal exists in main process, causing the config to not be persisted
|
||||
const pendingWorktreeConfigRef = useRef<TerminalWorktreeConfig | null>(null);
|
||||
// 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);
|
||||
|
||||
// Worktree dialog state
|
||||
const [showWorktreeDialog, setShowWorktreeDialog] = useState(false);
|
||||
@@ -128,9 +131,28 @@ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Termi
|
||||
terminalId: id,
|
||||
onCommandEnter: handleCommandEnter,
|
||||
onResize: (cols, rows) => {
|
||||
if (isCreatedRef.current) {
|
||||
window.electronAPI.resizeTerminal(id, cols, rows);
|
||||
// PTY dimension sync validation:
|
||||
// 1. Only resize if PTY is created
|
||||
// 2. Validate dimensions are within acceptable range
|
||||
// 3. Skip if dimensions haven't changed (prevents redundant IPC calls)
|
||||
if (!isCreatedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate dimensions are within acceptable range
|
||||
if (cols < MIN_COLS || rows < MIN_ROWS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip redundant resize calls if dimensions haven't changed
|
||||
const lastDims = lastPtyDimensionsRef.current;
|
||||
if (lastDims && lastDims.cols === cols && lastDims.rows === rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update tracked dimensions and send resize to PTY
|
||||
lastPtyDimensionsRef.current = { cols, rows };
|
||||
window.electronAPI.resizeTerminal(id, cols, rows);
|
||||
},
|
||||
onDimensionsReady: handleDimensionsReady,
|
||||
});
|
||||
@@ -168,6 +190,11 @@ 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 };
|
||||
}
|
||||
// If there's a pending worktree config from a recreation attempt,
|
||||
// sync it to main process now that the terminal exists.
|
||||
// This fixes the race condition where IPC calls happen before terminal creation.
|
||||
@@ -449,6 +476,10 @@ Please confirm you're ready by saying: I'm ready to work on ${selectedTask.title
|
||||
isCreatedRef.current = false;
|
||||
}
|
||||
|
||||
// Reset PTY dimension tracking for new terminal
|
||||
// This ensures the new PTY will receive initial dimensions correctly
|
||||
lastPtyDimensionsRef.current = null;
|
||||
|
||||
// Reset refs to allow recreation - effect will now trigger with new cwd
|
||||
resetForRecreate();
|
||||
}, [id, setWorktreeConfig, updateTerminal, prepareForRecreate, resetForRecreate]);
|
||||
|
||||
@@ -157,16 +157,27 @@ export function TerminalGrid({ projectPath, onNewTaskClick, isActive = false }:
|
||||
});
|
||||
|
||||
// Add each successfully restored session to the renderer's terminal store
|
||||
// Use staggered initialization to prevent race conditions when multiple terminals
|
||||
// try to initialize and measure dimensions simultaneously
|
||||
const TERMINAL_INIT_STAGGER_MS = 75; // Small delay between each terminal
|
||||
|
||||
for (const sessionResult of result.data.sessions) {
|
||||
if (sessionResult.success) {
|
||||
const fullSession = sortedSessions.find(s => s.id === sessionResult.id);
|
||||
if (fullSession) {
|
||||
console.warn(`[TerminalGrid] Adding restored terminal to store: ${fullSession.id}`);
|
||||
addRestoredTerminal(fullSession);
|
||||
// Stagger terminal initialization to prevent race conditions
|
||||
await new Promise(resolve => setTimeout(resolve, TERMINAL_INIT_STAGGER_MS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger terminal refit after grid layout stabilizes to ensure correct dimensions
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('terminal-refit-all'));
|
||||
}, TERMINAL_DOM_UPDATE_DELAY_MS);
|
||||
|
||||
// Refresh session dates to update counts
|
||||
const datesResult = await window.electronAPI.getTerminalSessionDates(projectPath);
|
||||
if (datesResult.success && datesResult.data) {
|
||||
|
||||
@@ -382,7 +382,7 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100); // 100ms debounce to prevent layout thrashing
|
||||
}, 200); // 200ms debounce for xterm.js resize stability (recommended minimum)
|
||||
|
||||
// Observe the terminalRef directly (not parent) for accurate resize detection
|
||||
const container = terminalRef.current;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { WebglAddon } from '@xterm/addon-webgl';
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
import { supportsWebGL2, getMaxWebGLContexts } from './webgl-utils';
|
||||
import { supportsWebGL2, getMaxWebGLContexts, isSafari } from './webgl-utils';
|
||||
|
||||
class WebGLContextManager {
|
||||
private static instance: WebGLContextManager;
|
||||
@@ -21,10 +21,18 @@ class WebGLContextManager {
|
||||
|
||||
private constructor() {
|
||||
// Check WebGL support once at startup
|
||||
this.isSupported = supportsWebGL2();
|
||||
// Skip WebGL on Safari due to known rendering issues with xterm.js WebGL addon
|
||||
// Safari will use Canvas renderer fallback for more stable rendering
|
||||
const safariDetected = isSafari();
|
||||
this.isSupported = !safariDetected && supportsWebGL2();
|
||||
// Use conservative max based on browser detection
|
||||
this.MAX_CONTEXTS = Math.min(getMaxWebGLContexts(), 8);
|
||||
|
||||
if (safariDetected) {
|
||||
console.warn(
|
||||
'[WebGLContextManager] Safari detected - WebGL disabled, using Canvas renderer fallback'
|
||||
);
|
||||
}
|
||||
console.warn(
|
||||
`[WebGLContextManager] Initialized - Supported: ${this.isSupported}, Max contexts: ${this.MAX_CONTEXTS}`
|
||||
);
|
||||
|
||||
@@ -31,6 +31,25 @@ export function supportsWebGL(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current browser is Safari
|
||||
* Note: Chrome's user agent also contains "Safari", so we need to exclude it
|
||||
*/
|
||||
export function isSafari(): boolean {
|
||||
try {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
// Safari includes "safari" but not "chrome" or "chromium"
|
||||
// Chrome/Chromium include both "safari" and "chrome"/"chromium"
|
||||
return (
|
||||
userAgent.includes('safari') &&
|
||||
!userAgent.includes('chrome') &&
|
||||
!userAgent.includes('chromium')
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum number of WebGL contexts supported by the browser
|
||||
* This is a conservative estimate - browsers typically support 8-16
|
||||
@@ -49,7 +68,7 @@ export function getMaxWebGLContexts(): number {
|
||||
} else if (userAgent.includes('firefox')) {
|
||||
// Firefox typically supports 32
|
||||
maxContexts = 32;
|
||||
} else if (userAgent.includes('safari')) {
|
||||
} else if (isSafari()) {
|
||||
// Safari is more conservative
|
||||
maxContexts = 8;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user