From 21512552393648cf37f951fe5412890c1eea73f0 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 14 Feb 2026 12:33:03 -0600 Subject: [PATCH] refactor: Simplify terminal rendering logic - Remove nested requestAnimationFrame calls - Simplify handleReplay: write + refresh + scrollToBottom - Simplify onBecameVisible: fit + refresh + focus - Remove excessive console.log statements - Convert async functions to sync where appropriate --- frontend/src/components/FloatingTerminal.vue | 49 +++---- .../src/composables/useTerminalRenderer.ts | 122 +++--------------- frontend/src/pages/TerminalPage.vue | 5 +- 3 files changed, 37 insertions(+), 139 deletions(-) diff --git a/frontend/src/components/FloatingTerminal.vue b/frontend/src/components/FloatingTerminal.vue index c791e81..276ba02 100644 --- a/frontend/src/components/FloatingTerminal.vue +++ b/frontend/src/components/FloatingTerminal.vue @@ -357,7 +357,6 @@ async function connect() { const connectionTimeout = window.setTimeout(() => { if (connecting.value && !connected.value) { - console.log('[Terminal] Connection timeout, retrying...') connecting.value = false socket?.close() socket = null @@ -384,24 +383,19 @@ async function connect() { } } - socket.onmessage = async (event) => { + socket.onmessage = (event) => { const msg = JSON.parse(event.data) if (msg.type === 'connected') { sessionId.value = msg.sessionId - console.log('[Terminal] Connected, hasHistory:', msg.hasHistory, 'bufferSize:', msg.bufferSize) - // Request replay if there's history and terminal is visible if (msg.hasHistory && isOpen.value) { - console.log('[Terminal] Requesting replay after connect...') setTimeout(() => requestReplay(), 50) } else if (!msg.isNew) { renderer.writeln('\x1b[36m[Session restored]\x1b[0m') } } else if (msg.type === 'replay') { - console.log('[Terminal] Received replay, length:', msg.data?.length) - // USE THE COMPOSABLE'S handleReplay! - await renderer.handleReplay(msg.data || '') + renderer.handleReplay(msg.data || '') } else if (msg.type === 'output') { // Token detection if (waitingForToken.value) { @@ -415,7 +409,6 @@ async function connect() { try { const decoded = atob(match[0]) JSON.parse(decoded) - console.log('[Terminal] WebMCP token detected') waitingForToken.value = false tokenBuffer = '' stopTokenPolling() @@ -465,7 +458,6 @@ function scheduleReconnect() { reconnectAttempts++ const delay = RECONNECT_DELAY_MS * Math.min(reconnectAttempts, 5) - console.log(`[Terminal] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`) reconnectTimeout = window.setTimeout(() => { if (isOpen.value && !connected.value && !connecting.value) { @@ -550,7 +542,6 @@ function scrollTerminal(direction: 'up' | 'down' | 'end') { function requestReplay(tailOnly = false) { if (socket?.readyState === WebSocket.OPEN) { - console.log('[Terminal] Requesting replay, tailOnly:', tailOnly) socket.send(JSON.stringify({ type: 'request-replay', tailOnly, chunks: 500 })) } } @@ -571,29 +562,22 @@ function refreshTerminal(fullReplay = false) { // WATCHERS // ============================================================================ -watch(isOpen, async (open) => { +watch(isOpen, (open) => { if (open) { - console.log('[Terminal] Opening terminal...') - await nextTick() + nextTick(() => { + renderer.init() - // Initialize terminal (only works when visible!) - renderer.init() + // Wait for CSS transition then setup + setTimeout(() => { + renderer.onBecameVisible() - // Wait for CSS transition, then connect - setTimeout(async () => { - console.log('[Terminal] Transition done, preparing terminal...') - - // Wait for terminal to be fully visible - await renderer.onBecameVisible() - console.log('[Terminal] Terminal visible, connecting...') - - // Now connect (or request replay if already connected) - if (!connected.value && !connecting.value) { - connect() - } else if (connected.value) { - requestReplay() - } - }, 180) + if (!connected.value && !connecting.value) { + connect() + } else if (connected.value) { + requestReplay() + } + }, 150) + }) } else { renderer.blur() waitingForToken.value = false @@ -618,9 +602,6 @@ onMounted(() => { checkMobile() window.addEventListener('resize', checkMobile) setupKeyboardDetection() - - // DON'T initialize terminal here - wait for isOpen! - console.log('[Terminal] Mounted, waiting for terminal to open...') }) onBeforeUnmount(() => { diff --git a/frontend/src/composables/useTerminalRenderer.ts b/frontend/src/composables/useTerminalRenderer.ts index c53d25b..fbb0248 100644 --- a/frontend/src/composables/useTerminalRenderer.ts +++ b/frontend/src/composables/useTerminalRenderer.ts @@ -93,11 +93,11 @@ export interface TerminalRenderer { // Buffer content getBufferContent: () => string - // Visibility handling - THE MAIN PROBLEM WE'RE SOLVING - onBecameVisible: () => Promise + // Visibility handling + onBecameVisible: () => void // Replay handling - handleReplay: (data: string) => Promise + handleReplay: (data: string) => void } // ============================================================================ @@ -158,37 +158,22 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR /** * Initialize the terminal. * IMPORTANT: Only call this when the container is VISIBLE! - * Returns true if initialization succeeded. */ function init(): boolean { - if (!options.container.value) { - console.warn('[TerminalRenderer] Init failed - no container') - return false - } + if (!options.container.value) return false + if (terminal.value) return true - if (terminal.value) { - console.log('[TerminalRenderer] Already initialized') - return true - } - - console.log('[TerminalRenderer] Initializing terminal...') - - // Create terminal with options terminal.value = new Terminal(getTerminalConfig(options)) - // Load addons fitAddon.value = new FitAddon() terminal.value.loadAddon(fitAddon.value) terminal.value.loadAddon(new WebLinksAddon()) - // Open terminal in container terminal.value.open(options.container.value) - // Initial fit (use nextTick to ensure DOM is ready) nextTick(() => { fit() isReady.value = true - console.log('[TerminalRenderer] Ready, cols:', terminal.value?.cols, 'rows:', terminal.value?.rows) }) // Setup resize observer @@ -220,7 +205,6 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR * Dispose the terminal and cleanup resources. */ function dispose(): void { - console.log('[TerminalRenderer] Disposing...') resizeObserver?.disconnect() resizeObserver = null terminal.value?.dispose() @@ -235,19 +219,14 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR /** * Write data to terminal with Promise-based completion. - * The promise resolves when the data has been rendered. */ function write(data: string): Promise { return new Promise((resolve) => { if (!terminal.value) { - console.warn('[TerminalRenderer] Write failed - no terminal') resolve() return } - - terminal.value.write(data, () => { - resolve() - }) + terminal.value.write(data, resolve) }) } @@ -266,15 +245,13 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR * Clear the terminal scrollback buffer. */ function clear(): void { - console.log('[TerminalRenderer] Clearing buffer') terminal.value?.clear() } /** - * Reset the terminal completely (more aggressive than clear). + * Reset the terminal completely. */ function reset(): void { - console.log('[TerminalRenderer] Resetting terminal') terminal.value?.reset() } @@ -295,9 +272,7 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR */ function refresh(): void { if (!terminal.value) return - const rows = terminal.value.rows || 24 - console.log('[TerminalRenderer] Refreshing rows 0 to', rows - 1) - terminal.value.refresh(0, rows - 1) + terminal.value.refresh(0, terminal.value.rows - 1) } /** @@ -360,40 +335,14 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR /** * Handle terminal becoming visible after being hidden. - * - * When terminal container goes from hidden to visible, xterm.js has issues: - * 1. Dimensions were calculated as 0x0 when hidden - * 2. Content may not render correctly - * 3. Scroll position may be wrong - * - * This function should be called when the terminal becomes visible. - * It uses multiple requestAnimationFrame to ensure browser has painted. + * Simple: fit + refresh + focus */ - async function onBecameVisible(): Promise { - console.log('[TerminalRenderer] onBecameVisible called') + function onBecameVisible(): void { + if (!terminal.value || !fitAddon.value) return - if (!terminal.value || !fitAddon.value) { - console.warn('[TerminalRenderer] Terminal not initialized') - return - } - - return new Promise((resolve) => { - // Triple RAF to ensure browser has fully painted - requestAnimationFrame(() => { - console.log('[TerminalRenderer] RAF 1: Fitting...') - fit() - - requestAnimationFrame(() => { - console.log('[TerminalRenderer] RAF 2: Dimensions:', terminal.value?.cols, 'x', terminal.value?.rows) - - requestAnimationFrame(() => { - console.log('[TerminalRenderer] RAF 3: Ready for content') - focus() - resolve() - }) - }) - }) - }) + fit() + terminal.value.refresh(0, terminal.value.rows - 1) + focus() } // ========================================================================== @@ -402,45 +351,14 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR /** * Handle replay data from server. - * This is the critical function for solving the rendering issue. - * - * Steps: - * 1. Reset terminal to clean state - * 2. Fit to ensure correct dimensions - * 3. Write data - * 4. Wait for write to complete - * 5. Scroll to bottom using multiple RAF + * Simple: write + refresh + scrollToBottom */ - async function handleReplay(data: string): Promise { - console.log('[TerminalRenderer] handleReplay, bytes:', data.length) + function handleReplay(data: string): void { + if (!terminal.value) return - if (!terminal.value || !fitAddon.value) { - console.warn('[TerminalRenderer] Cannot handle replay - terminal not ready') - return - } - - // Step 1: Reset terminal completely - reset() - - // Step 2: Fit to get correct dimensions - fit() - console.log('[TerminalRenderer] Pre-write dimensions:', terminal.value.cols, 'x', terminal.value.rows) - - // Step 3 & 4: Write data and wait for completion - await write(data) - console.log('[TerminalRenderer] Write completed') - - // Step 5: Scroll to bottom with multiple RAF to ensure rendering - return new Promise((resolve) => { - requestAnimationFrame(() => { - fit() - - requestAnimationFrame(() => { - scrollToBottom() - console.log('[TerminalRenderer] Scroll completed') - resolve() - }) - }) + terminal.value.write(data, () => { + terminal.value?.refresh(0, terminal.value.rows - 1) + terminal.value?.scrollToBottom() }) } diff --git a/frontend/src/pages/TerminalPage.vue b/frontend/src/pages/TerminalPage.vue index ad832cb..0102b82 100644 --- a/frontend/src/pages/TerminalPage.vue +++ b/frontend/src/pages/TerminalPage.vue @@ -77,7 +77,7 @@ async function connect() { } } - socket.onmessage = async (event) => { + socket.onmessage = (event) => { const msg = JSON.parse(event.data) if (msg.type === 'connected') { @@ -85,13 +85,12 @@ async function connect() { isResumedSession.value = !msg.isNew if (msg.hasHistory) { - // Request replay socket?.send(JSON.stringify({ type: 'request-replay', tailOnly: false })) } else if (!msg.isNew) { renderer.writeln('\x1b[36m[Reconnected to existing session]\x1b[0m') } } else if (msg.type === 'replay') { - await renderer.handleReplay(msg.data || '') + renderer.handleReplay(msg.data || '') } else if (msg.type === 'output') { renderer.write(msg.data) } else if (msg.type === 'exit') {