refactor: Extract terminal rendering logic to useTerminalRenderer composable

- Create useTerminalRenderer.ts with all xterm.js logic
- Support custom theme, fontSize, fontFamily options
- Add handleReplay() for proper visibility handling
- Add getBufferContent() for copying terminal content
- Refactor FloatingTerminal.vue to use composable
- Refactor TerminalPage.vue to use composable
- Server: Add request-replay message type for on-demand replay
- Server: Remove auto-replay on connect (client requests when ready)
- Fix xterm.js rendering issues with hidden containers (v-show)
This commit is contained in:
2026-02-14 12:16:34 -06:00
parent e3ce3712b5
commit 303755437d
5 changed files with 877 additions and 770 deletions

View File

@@ -7,7 +7,7 @@ export const WORKING_DIR = process.cwd().replace(/[\\\/]server$/, '')
export const SHELL = process.platform === 'win32' ? 'powershell.exe' : 'bash'
export const SHELL_ARGS = process.platform === 'win32' ? ['-NoLogo', '-NoProfile'] : []
export const DEFAULT_SESSION_ID = 'main'
export const MAX_BUFFER_LINES = 1000
export const MAX_BUFFER_LINES = 10000
// Database
export const DB_PATH = 'agent-ui.db'

View File

@@ -155,21 +155,19 @@ export function startTerminalServer() {
session.clients.add(ws)
wsToSession.set(ws, sessionId)
// Send connection info
// Send connection info (include buffer size so client knows if replay is needed)
ws.send(JSON.stringify({
type: 'connected',
sessionId: session.id,
isNew: session.outputBuffer.length === 0
isNew: session.outputBuffer.length === 0,
hasHistory: session.outputBuffer.length > 0,
bufferSize: session.outputBuffer.length
}))
// Replay buffer if there's history
if (session.outputBuffer.length > 0) {
console.log(`[Terminal] Replaying ${session.outputBuffer.length} buffer entries`)
ws.send(JSON.stringify({
type: 'replay',
data: session.outputBuffer.join('')
}))
}
// DON'T auto-replay here!
// Client will request replay when terminal is visible and ready.
// This fixes xterm.js rendering issues with hidden containers.
console.log(`[Terminal] Client connected, buffer has ${session.outputBuffer.length} chunks (client will request replay)`)
console.log(`[Terminal] Client joined session ${sessionId} (${session.clients.size} clients)`)
} catch (e: any) {
@@ -191,6 +189,33 @@ 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 === 'request-replay') {
// Client requests fresh replay (used when terminal becomes visible)
console.log(`[Terminal] Replay requested, buffer has ${session.outputBuffer.length} chunks`)
if (session.outputBuffer.length > 0) {
// If tailOnly specified, only send last N chunks (enough for a few screens)
const tailOnly = msg.tailOnly === true
const tailChunks = msg.chunks || 500 // Default ~500 chunks for tail
let data: string
if (tailOnly && session.outputBuffer.length > tailChunks) {
// Send only the tail - more efficient for large buffers
data = session.outputBuffer.slice(-tailChunks).join('')
console.log(`[Terminal] Replaying tail (${tailChunks}/${session.outputBuffer.length} chunks), ${data.length} bytes`)
} else {
data = session.outputBuffer.join('')
console.log(`[Terminal] Replaying full buffer (${session.outputBuffer.length} chunks), ${data.length} bytes`)
}
ws.send(JSON.stringify({
type: 'replay',
data,
isTail: tailOnly && session.outputBuffer.length > tailChunks
}))
} else {
console.log('[Terminal] No buffer to replay')
}
}
} catch (e: any) {
console.error('[Terminal] Error:', e)