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:
@@ -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(() => {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
Reference in New Issue
Block a user