diff --git a/frontend/src/components/FloatingTerminal.vue b/frontend/src/components/FloatingTerminal.vue index 276ba02..9f92fff 100644 --- a/frontend/src/components/FloatingTerminal.vue +++ b/frontend/src/components/FloatingTerminal.vue @@ -506,6 +506,13 @@ function sendClear() { } } +function clearServerBuffer() { + if (socket?.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'clear-buffer' })) + renderer.reset() + } +} + function requestToken() { if (socket?.readyState === WebSocket.OPEN) { tokenBuffer = '' @@ -725,6 +732,7 @@ defineExpose({
+
@@ -862,6 +870,7 @@ defineExpose({ } .window-controls button:hover { background: rgba(255,255,255,0.5); } .window-controls button.x:hover { background: linear-gradient(180deg, #e66 0%, #c33 100%); border-color: #a22; color: #fff; } +.window-controls button.clear-buf:hover { background: linear-gradient(180deg, #f90 0%, #c60 100%); border-color: #a50; color: #fff; } .window-controls button.waiting { background: rgba(16, 185, 129, 0.3); border-color: #10b981; animation: pulse 0.8s infinite; } .content { diff --git a/frontend/src/composables/useTerminalRenderer.ts b/frontend/src/composables/useTerminalRenderer.ts index fbb0248..288acec 100644 --- a/frontend/src/composables/useTerminalRenderer.ts +++ b/frontend/src/composables/useTerminalRenderer.ts @@ -171,16 +171,23 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR terminal.value.open(options.container.value) + // Don't fit here - wait for onBecameVisible() when container is fully sized nextTick(() => { - fit() isReady.value = true }) - // Setup resize observer + // Setup resize observer (ignore tiny sizes during transitions) resizeObserver = new ResizeObserver(() => { if (fitAddon.value && terminal.value) { + const prevCols = terminal.value.cols + const prevRows = terminal.value.rows fitAddon.value.fit() - options.onResize?.(terminal.value.cols, terminal.value.rows) + // Only notify if size is reasonable (not during CSS transitions) + if (terminal.value.cols >= 20 && terminal.value.rows >= 5) { + if (terminal.value.cols !== prevCols || terminal.value.rows !== prevRows) { + options.onResize?.(terminal.value.cols, terminal.value.rows) + } + } } }) resizeObserver.observe(options.container.value) @@ -351,15 +358,42 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR /** * Handle replay data from server. - * Simple: write + refresh + scrollToBottom + * Writes in chunks to avoid overwhelming xterm.js with large buffers. */ function handleReplay(data: string): void { if (!terminal.value) return - terminal.value.write(data, () => { - terminal.value?.refresh(0, terminal.value.rows - 1) - terminal.value?.scrollToBottom() - }) + const CHUNK_SIZE = 8192 // 8KB chunks + const CHUNK_DELAY = 10 // 10ms between chunks + + // Small data: write directly + if (data.length <= CHUNK_SIZE) { + terminal.value.write(data, () => { + terminal.value?.refresh(0, terminal.value.rows - 1) + terminal.value?.scrollToBottom() + }) + return + } + + // Large data: write in chunks + let offset = 0 + const writeNextChunk = () => { + if (!terminal.value || offset >= data.length) { + // Done - final refresh and scroll + terminal.value?.refresh(0, terminal.value.rows - 1) + terminal.value?.scrollToBottom() + return + } + + const chunk = data.slice(offset, offset + CHUNK_SIZE) + offset += CHUNK_SIZE + + terminal.value.write(chunk, () => { + setTimeout(writeNextChunk, CHUNK_DELAY) + }) + } + + writeNextChunk() } // ========================================================================== diff --git a/server/services/terminal.ts b/server/services/terminal.ts index e5a9b4a..5b3fdcc 100644 --- a/server/services/terminal.ts +++ b/server/services/terminal.ts @@ -187,6 +187,10 @@ export function startTerminalServer() { } else if (msg.type === 'resize' && msg.cols && msg.rows) { session.pty.resize(msg.cols, msg.rows) console.log(`[Terminal] Session ${sessionId} resized to ${msg.cols}x${msg.rows}`) + } else if (msg.type === 'clear-buffer') { + session.outputBuffer = [] + console.log(`[Terminal] Buffer cleared for session ${sessionId}`) + ws.send(JSON.stringify({ type: 'buffer-cleared' })) } else if (msg.type === 'request-replay') { // Client requests fresh replay (used when terminal becomes visible) console.log(`[Terminal] Replay requested, buffer has ${session.outputBuffer.length} chunks`)