feat(terminal): implement resize handling with ResizeObserver for improved terminal responsiveness

This commit is contained in:
Andras Bacsai
2025-06-06 22:05:16 +02:00
parent 6aa82817df
commit 6e85419adb
2 changed files with 99 additions and 19 deletions

View File

@@ -30,6 +30,9 @@ export function initializeTerminalComponent() {
pingTimeoutId: null,
heartbeatMissed: 0,
maxHeartbeatMisses: 3,
// Resize handling
resizeObserver: null,
resizeTimeout: null,
init() {
this.setupTerminal();
@@ -55,8 +58,18 @@ export function initializeTerminalComponent() {
if (active) {
this.$refs.terminalWrapper.style.display = 'block';
this.resizeTerminal();
// Start observing terminal wrapper for resize changes
if (this.resizeObserver && this.$refs.terminalWrapper) {
this.resizeObserver.observe(this.$refs.terminalWrapper);
}
} else {
this.$refs.terminalWrapper.style.display = 'none';
// Stop observing when terminal is inactive
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
});
});
@@ -70,6 +83,17 @@ export function initializeTerminalComponent() {
window.onresize = () => {
this.resizeTerminal()
};
// Set up ResizeObserver for more reliable terminal resizing
if (window.ResizeObserver) {
this.resizeObserver = new ResizeObserver(() => {
// Debounce resize calls to avoid performance issues
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
this.resizeTerminal();
}, 50);
});
}
},
cleanup() {
@@ -79,15 +103,27 @@ export function initializeTerminalComponent() {
if (this.socket) {
this.socket.close(1000, 'Client cleanup');
}
// Clean up resize observer
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
// Clear resize timeout
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
},
clearAllTimers() {
[this.keepAliveInterval, this.reconnectInterval, this.connectionTimeoutId, this.pingTimeoutId]
[this.keepAliveInterval, this.reconnectInterval, this.connectionTimeoutId, this.pingTimeoutId, this.resizeTimeout]
.forEach(timer => timer && clearInterval(timer));
this.keepAliveInterval = null;
this.reconnectInterval = null;
this.connectionTimeoutId = null;
this.pingTimeoutId = null;
this.resizeTimeout = null;
},
resetTerminal() {
@@ -308,8 +344,15 @@ export function initializeTerminalComponent() {
this.terminalActive = true;
this.term.focus();
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded-sm');
// Initial resize after terminal is ready
this.resizeTerminal();
// Additional resize after a short delay to ensure proper sizing
setTimeout(() => {
this.resizeTerminal();
}, 200);
// Notify parent component that terminal is connected
this.$wire.dispatch('terminalConnected');
} else if (event.data === 'unprocessable') {
@@ -418,7 +461,13 @@ export function initializeTerminalComponent() {
makeFullscreen() {
this.fullscreen = !this.fullscreen;
this.$nextTick(() => {
this.resizeTerminal();
// Force a layout reflow to ensure DOM changes are applied
this.$refs.terminalWrapper.offsetHeight;
// Add a small delay to ensure CSS transitions complete
setTimeout(() => {
this.resizeTerminal();
}, 100);
});
},
@@ -426,25 +475,53 @@ export function initializeTerminalComponent() {
if (!this.terminalActive || !this.term || !this.fitAddon) return;
try {
// Force a refresh of the fit addon dimensions
this.fitAddon.fit();
const height = this.$refs.terminalWrapper.clientHeight;
const width = this.$refs.terminalWrapper.clientWidth;
const charSize = this.term._core._renderService._charSizeService;
if (!charSize.height || !charSize.width) {
// Fallback values if char size not available yet
// Get fresh dimensions after fit
const wrapperHeight = this.$refs.terminalWrapper.clientHeight;
const wrapperWidth = this.$refs.terminalWrapper.clientWidth;
// Account for terminal container padding (px-2 py-1 = 8px left/right, 4px top/bottom)
const horizontalPadding = 16; // 8px * 2 (left + right)
const verticalPadding = 8; // 4px * 2 (top + bottom)
const height = wrapperHeight - verticalPadding;
const width = wrapperWidth - horizontalPadding;
// Check if dimensions are valid
if (height <= 0 || width <= 0) {
console.warn('[Terminal] Invalid wrapper dimensions, retrying...', { height, width });
setTimeout(() => this.resizeTerminal(), 100);
return;
}
const charSize = this.term._core._renderService._charSizeService;
if (!charSize.height || !charSize.width) {
// Fallback values if char size not available yet
console.warn('[Terminal] Character size not available, retrying...');
setTimeout(() => this.resizeTerminal(), 100);
return;
}
// Calculate new dimensions with padding considerations
const rows = Math.floor(height / charSize.height) - 1;
const cols = Math.floor(width / charSize.width) - 1;
if (rows > 0 && cols > 0) {
this.term.resize(cols, rows);
this.sendMessage({
resize: { cols: cols, rows: rows }
});
// Check if dimensions actually changed to avoid unnecessary resizes
const currentCols = this.term.cols;
const currentRows = this.term.rows;
if (cols !== currentCols || rows !== currentRows) {
console.log(`[Terminal] Resizing terminal: ${currentCols}x${currentRows} -> ${cols}x${rows}`);
this.term.resize(cols, rows);
this.sendMessage({
resize: { cols: cols, rows: rows }
});
}
} else {
console.warn('[Terminal] Invalid calculated dimensions:', { rows, cols, height, width, charSize });
}
} catch (error) {
console.error('[Terminal] Resize error:', error);