fix: Improve terminal buffer handling and replay performance

- Add chunked replay (8KB chunks with 10ms delay) to avoid overwhelming xterm.js
- Add clear-buffer server command to reset terminal history
- Add clear buffer button in terminal header
- Filter out tiny resize events during CSS transitions (ignore < 20x5)
- Remove premature fit() call from init - wait for onBecameVisible
This commit is contained in:
2026-02-14 12:57:30 -06:00
parent 88a76c005d
commit 2a01574d00
3 changed files with 55 additions and 8 deletions

View File

@@ -506,6 +506,13 @@ function sendClear() {
} }
} }
function clearServerBuffer() {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'clear-buffer' }))
renderer.reset()
}
}
function requestToken() { function requestToken() {
if (socket?.readyState === WebSocket.OPEN) { if (socket?.readyState === WebSocket.OPEN) {
tokenBuffer = '' tokenBuffer = ''
@@ -725,6 +732,7 @@ defineExpose({
<div class="window-controls"> <div class="window-controls">
<button @click="requestToken" :class="{ waiting: waitingForToken }" title="Connect MCP"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button> <button @click="requestToken" :class="{ waiting: waitingForToken }" title="Connect MCP"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
<button @click="runClaude" title="Claude"><svg width="8" height="8" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></button> <button @click="runClaude" title="Claude"><svg width="8" height="8" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></button>
<button @click="clearServerBuffer" class="clear-buf" title="Clear Buffer"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14"/></svg></button>
<button @click="refreshTerminal" title="Refresh Screen"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg></button> <button @click="refreshTerminal" title="Refresh Screen"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg></button>
<button class="x" @click="close" title="Close"><svg width="8" height="8" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/></svg></button> <button class="x" @click="close" title="Close"><svg width="8" height="8" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/></svg></button>
</div> </div>
@@ -862,6 +870,7 @@ defineExpose({
} }
.window-controls button:hover { background: rgba(255,255,255,0.5); } .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.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; } .window-controls button.waiting { background: rgba(16, 185, 129, 0.3); border-color: #10b981; animation: pulse 0.8s infinite; }
.content { .content {

View File

@@ -171,17 +171,24 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR
terminal.value.open(options.container.value) terminal.value.open(options.container.value)
// Don't fit here - wait for onBecameVisible() when container is fully sized
nextTick(() => { nextTick(() => {
fit()
isReady.value = true isReady.value = true
}) })
// Setup resize observer // Setup resize observer (ignore tiny sizes during transitions)
resizeObserver = new ResizeObserver(() => { resizeObserver = new ResizeObserver(() => {
if (fitAddon.value && terminal.value) { if (fitAddon.value && terminal.value) {
const prevCols = terminal.value.cols
const prevRows = terminal.value.rows
fitAddon.value.fit() fitAddon.value.fit()
// 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) options.onResize?.(terminal.value.cols, terminal.value.rows)
} }
}
}
}) })
resizeObserver.observe(options.container.value) resizeObserver.observe(options.container.value)
@@ -351,15 +358,42 @@ export function useTerminalRenderer(options: TerminalRendererOptions): TerminalR
/** /**
* Handle replay data from server. * 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 { function handleReplay(data: string): void {
if (!terminal.value) return if (!terminal.value) return
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.write(data, () => {
terminal.value?.refresh(0, terminal.value.rows - 1) terminal.value?.refresh(0, terminal.value.rows - 1)
terminal.value?.scrollToBottom() 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()
} }
// ========================================================================== // ==========================================================================

View File

@@ -187,6 +187,10 @@ export function startTerminalServer() {
} else if (msg.type === 'resize' && msg.cols && msg.rows) { } else if (msg.type === 'resize' && msg.cols && msg.rows) {
session.pty.resize(msg.cols, msg.rows) session.pty.resize(msg.cols, msg.rows)
console.log(`[Terminal] Session ${sessionId} resized to ${msg.cols}x${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') { } else if (msg.type === 'request-replay') {
// Client requests fresh replay (used when terminal becomes visible) // Client requests fresh replay (used when terminal becomes visible)
console.log(`[Terminal] Replay requested, buffer has ${session.outputBuffer.length} chunks`) console.log(`[Terminal] Replay requested, buffer has ${session.outputBuffer.length} chunks`)