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
This commit is contained in:
2026-02-14 12:33:03 -06:00
parent 303755437d
commit 2151255239
3 changed files with 37 additions and 139 deletions

View File

@@ -357,7 +357,6 @@ async function connect() {
const connectionTimeout = window.setTimeout(() => { const connectionTimeout = window.setTimeout(() => {
if (connecting.value && !connected.value) { if (connecting.value && !connected.value) {
console.log('[Terminal] Connection timeout, retrying...')
connecting.value = false connecting.value = false
socket?.close() socket?.close()
socket = null socket = null
@@ -384,24 +383,19 @@ async function connect() {
} }
} }
socket.onmessage = async (event) => { socket.onmessage = (event) => {
const msg = JSON.parse(event.data) const msg = JSON.parse(event.data)
if (msg.type === 'connected') { if (msg.type === 'connected') {
sessionId.value = msg.sessionId 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) { if (msg.hasHistory && isOpen.value) {
console.log('[Terminal] Requesting replay after connect...')
setTimeout(() => requestReplay(), 50) setTimeout(() => requestReplay(), 50)
} else if (!msg.isNew) { } else if (!msg.isNew) {
renderer.writeln('\x1b[36m[Session restored]\x1b[0m') renderer.writeln('\x1b[36m[Session restored]\x1b[0m')
} }
} else if (msg.type === 'replay') { } else if (msg.type === 'replay') {
console.log('[Terminal] Received replay, length:', msg.data?.length) renderer.handleReplay(msg.data || '')
// USE THE COMPOSABLE'S handleReplay!
await renderer.handleReplay(msg.data || '')
} else if (msg.type === 'output') { } else if (msg.type === 'output') {
// Token detection // Token detection
if (waitingForToken.value) { if (waitingForToken.value) {
@@ -415,7 +409,6 @@ async function connect() {
try { try {
const decoded = atob(match[0]) const decoded = atob(match[0])
JSON.parse(decoded) JSON.parse(decoded)
console.log('[Terminal] WebMCP token detected')
waitingForToken.value = false waitingForToken.value = false
tokenBuffer = '' tokenBuffer = ''
stopTokenPolling() stopTokenPolling()
@@ -465,7 +458,6 @@ function scheduleReconnect() {
reconnectAttempts++ reconnectAttempts++
const delay = RECONNECT_DELAY_MS * Math.min(reconnectAttempts, 5) const delay = RECONNECT_DELAY_MS * Math.min(reconnectAttempts, 5)
console.log(`[Terminal] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`)
reconnectTimeout = window.setTimeout(() => { reconnectTimeout = window.setTimeout(() => {
if (isOpen.value && !connected.value && !connecting.value) { if (isOpen.value && !connected.value && !connecting.value) {
@@ -550,7 +542,6 @@ function scrollTerminal(direction: 'up' | 'down' | 'end') {
function requestReplay(tailOnly = false) { function requestReplay(tailOnly = false) {
if (socket?.readyState === WebSocket.OPEN) { if (socket?.readyState === WebSocket.OPEN) {
console.log('[Terminal] Requesting replay, tailOnly:', tailOnly)
socket.send(JSON.stringify({ type: 'request-replay', tailOnly, chunks: 500 })) socket.send(JSON.stringify({ type: 'request-replay', tailOnly, chunks: 500 }))
} }
} }
@@ -571,29 +562,22 @@ function refreshTerminal(fullReplay = false) {
// WATCHERS // WATCHERS
// ============================================================================ // ============================================================================
watch(isOpen, async (open) => { watch(isOpen, (open) => {
if (open) { if (open) {
console.log('[Terminal] Opening terminal...') nextTick(() => {
await nextTick() renderer.init()
// Initialize terminal (only works when visible!) // Wait for CSS transition then setup
renderer.init() setTimeout(() => {
renderer.onBecameVisible()
// Wait for CSS transition, then connect if (!connected.value && !connecting.value) {
setTimeout(async () => { connect()
console.log('[Terminal] Transition done, preparing terminal...') } else if (connected.value) {
requestReplay()
// Wait for terminal to be fully visible }
await renderer.onBecameVisible() }, 150)
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)
} else { } else {
renderer.blur() renderer.blur()
waitingForToken.value = false waitingForToken.value = false
@@ -618,9 +602,6 @@ onMounted(() => {
checkMobile() checkMobile()
window.addEventListener('resize', checkMobile) window.addEventListener('resize', checkMobile)
setupKeyboardDetection() setupKeyboardDetection()
// DON'T initialize terminal here - wait for isOpen!
console.log('[Terminal] Mounted, waiting for terminal to open...')
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@@ -93,11 +93,11 @@ export interface TerminalRenderer {
// Buffer content // Buffer content
getBufferContent: () => string getBufferContent: () => string
// Visibility handling - THE MAIN PROBLEM WE'RE SOLVING // Visibility handling
onBecameVisible: () => Promise<void> onBecameVisible: () => void
// Replay handling // Replay handling
handleReplay: (data: string) => Promise<void> handleReplay: (data: string) => void
} }
// ============================================================================ // ============================================================================
@@ -158,37 +158,22 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR
/** /**
* Initialize the terminal. * Initialize the terminal.
* IMPORTANT: Only call this when the container is VISIBLE! * IMPORTANT: Only call this when the container is VISIBLE!
* Returns true if initialization succeeded.
*/ */
function init(): boolean { function init(): boolean {
if (!options.container.value) { if (!options.container.value) return false
console.warn('[TerminalRenderer] Init failed - no container') if (terminal.value) return true
return false
}
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)) terminal.value = new Terminal(getTerminalConfig(options))
// Load addons
fitAddon.value = new FitAddon() fitAddon.value = new FitAddon()
terminal.value.loadAddon(fitAddon.value) terminal.value.loadAddon(fitAddon.value)
terminal.value.loadAddon(new WebLinksAddon()) terminal.value.loadAddon(new WebLinksAddon())
// Open terminal in container
terminal.value.open(options.container.value) terminal.value.open(options.container.value)
// Initial fit (use nextTick to ensure DOM is ready)
nextTick(() => { nextTick(() => {
fit() fit()
isReady.value = true isReady.value = true
console.log('[TerminalRenderer] Ready, cols:', terminal.value?.cols, 'rows:', terminal.value?.rows)
}) })
// Setup resize observer // Setup resize observer
@@ -220,7 +205,6 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR
* Dispose the terminal and cleanup resources. * Dispose the terminal and cleanup resources.
*/ */
function dispose(): void { function dispose(): void {
console.log('[TerminalRenderer] Disposing...')
resizeObserver?.disconnect() resizeObserver?.disconnect()
resizeObserver = null resizeObserver = null
terminal.value?.dispose() terminal.value?.dispose()
@@ -235,19 +219,14 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR
/** /**
* Write data to terminal with Promise-based completion. * Write data to terminal with Promise-based completion.
* The promise resolves when the data has been rendered.
*/ */
function write(data: string): Promise<void> { function write(data: string): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!terminal.value) { if (!terminal.value) {
console.warn('[TerminalRenderer] Write failed - no terminal')
resolve() resolve()
return 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. * Clear the terminal scrollback buffer.
*/ */
function clear(): void { function clear(): void {
console.log('[TerminalRenderer] Clearing buffer')
terminal.value?.clear() terminal.value?.clear()
} }
/** /**
* Reset the terminal completely (more aggressive than clear). * Reset the terminal completely.
*/ */
function reset(): void { function reset(): void {
console.log('[TerminalRenderer] Resetting terminal')
terminal.value?.reset() terminal.value?.reset()
} }
@@ -295,9 +272,7 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR
*/ */
function refresh(): void { function refresh(): void {
if (!terminal.value) return if (!terminal.value) return
const rows = terminal.value.rows || 24 terminal.value.refresh(0, terminal.value.rows - 1)
console.log('[TerminalRenderer] Refreshing rows 0 to', rows - 1)
terminal.value.refresh(0, rows - 1)
} }
/** /**
@@ -360,40 +335,14 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR
/** /**
* Handle terminal becoming visible after being hidden. * Handle terminal becoming visible after being hidden.
* * Simple: fit + refresh + focus
* 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.
*/ */
async function onBecameVisible(): Promise<void> { function onBecameVisible(): void {
console.log('[TerminalRenderer] onBecameVisible called') if (!terminal.value || !fitAddon.value) return
if (!terminal.value || !fitAddon.value) { fit()
console.warn('[TerminalRenderer] Terminal not initialized') terminal.value.refresh(0, terminal.value.rows - 1)
return focus()
}
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()
})
})
})
})
} }
// ========================================================================== // ==========================================================================
@@ -402,45 +351,14 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR
/** /**
* Handle replay data from server. * Handle replay data from server.
* This is the critical function for solving the rendering issue. * Simple: write + refresh + scrollToBottom
*
* 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
*/ */
async function handleReplay(data: string): Promise<void> { function handleReplay(data: string): void {
console.log('[TerminalRenderer] handleReplay, bytes:', data.length) if (!terminal.value) return
if (!terminal.value || !fitAddon.value) { terminal.value.write(data, () => {
console.warn('[TerminalRenderer] Cannot handle replay - terminal not ready') terminal.value?.refresh(0, terminal.value.rows - 1)
return terminal.value?.scrollToBottom()
}
// 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()
})
})
}) })
} }

View File

@@ -77,7 +77,7 @@ async function connect() {
} }
} }
socket.onmessage = async (event) => { socket.onmessage = (event) => {
const msg = JSON.parse(event.data) const msg = JSON.parse(event.data)
if (msg.type === 'connected') { if (msg.type === 'connected') {
@@ -85,13 +85,12 @@ async function connect() {
isResumedSession.value = !msg.isNew isResumedSession.value = !msg.isNew
if (msg.hasHistory) { if (msg.hasHistory) {
// Request replay
socket?.send(JSON.stringify({ type: 'request-replay', tailOnly: false })) socket?.send(JSON.stringify({ type: 'request-replay', tailOnly: false }))
} else if (!msg.isNew) { } else if (!msg.isNew) {
renderer.writeln('\x1b[36m[Reconnected to existing session]\x1b[0m') renderer.writeln('\x1b[36m[Reconnected to existing session]\x1b[0m')
} }
} else if (msg.type === 'replay') { } else if (msg.type === 'replay') {
await renderer.handleReplay(msg.data || '') renderer.handleReplay(msg.data || '')
} else if (msg.type === 'output') { } else if (msg.type === 'output') {
renderer.write(msg.data) renderer.write(msg.data)
} else if (msg.type === 'exit') { } else if (msg.type === 'exit') {