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:
@@ -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 {
|
||||||
|
|||||||
@@ -171,16 +171,23 @@ 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()
|
||||||
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)
|
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
|
||||||
|
|
||||||
terminal.value.write(data, () => {
|
const CHUNK_SIZE = 8192 // 8KB chunks
|
||||||
terminal.value?.refresh(0, terminal.value.rows - 1)
|
const CHUNK_DELAY = 10 // 10ms between chunks
|
||||||
terminal.value?.scrollToBottom()
|
|
||||||
})
|
// 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
Reference in New Issue
Block a user